Optimistic locking for string objects - compare-and-set and compare-and-delete (#14435)

# Description

Add optimistic locking for string objects via compare-and-set and
compare-and-delete mechanism.

## What's changed

Introduction of new DIGEST command for string objects calculated via
XXH3 hash.

Extend SET command with new parameters supporting optimistic locking.
The new value is set only if checks against a given (old) value or a
given string digest pass.

Introduction of new DELEX command to support conditionally deleting a
key. Conditions are also checks against string value or string digest.

## Motivation

For developers who need to to implement a compare-and-set and
compare-and-delete single-key optimistic concurrency control this PR
provides single-command based implementation.

Compare-and-set and compare-and-delete are mostly used for [Optimistic
concurrency
control](https://en.wikipedia.org/wiki/Optimistic_concurrency_control):
a client (1) fetches the value, keeps the old value (or its digest, for
a large string) in memory, (2) manipulates a local copy of the value,
(3) applies the local changes to the server, but only if the server’s
value hasn’t been changed (still equal to the old value).

Note that compare-and-set [can also be
implemented](https://redis.io/docs/latest/develop/using-commands/transactions/#optimistic-locking-using-check-and-set)
with WATCH … MULTI … EXEC and Lua scripts. The new SET optional
arguments and the DELEX command do not enable new functionality,
however, they are much simpler and faster to use for the very common use
case of single-key optimistic concurrency control.

## Related issues and PRs

https://github.com/redis/redis/issues/12485
https://github.com/redis/redis/pull/8361
https://github.com/redis/redis/pull/4258

## Description of the new commands

### DIGEST

```
DIGEST key
```

Get the hash digest of the value stored in key, as an hex string.

Reply:
- Null if key does not exist
- error if key exists but holds a value which is not a string
- (bulk string) the XXH3 digest of the value stored in key, as an hex
string

### SET

```
SET key value [NX | XX | IFEQ match-value | IFNE match-value | IFDEQ match-digest | IFDNE match-digest] [GET] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]
```

`IFEQ match-value` - Set the key’s value and expiration only if its
current value is equal to match-value. If key doesn’t exist - it won’t
be created.
`IFNE match-value` - Set the key’s value and expiration only if its
current value is not equal to match-value. If key doesn’t exist - it
will be created.
`IFDEQ match-digest` - Set the key’s value and expiration only if the
digest of its current value is equal to match-digest. If key doesn’t
exist - it won’t be created.
`IFDNE match-digest` - Set the key’s value and expiration only if the
digest of its current value is not equal to match-digest. If key doesn’t
exist - it will be created.

Reply update:
- If GET was not specified:
   - Nil reply if either
- the key doesn’t exist and XX/IFEQ/IFDEQ was specified. The key was not
created.
- the key exists, and NX was specified or a specified
IFEQ/IFNE/IFDEQ/IFDNE condition is false. The key was not set.
   - Simple string reply: OK: The key was set.
- If GET was specified, any of the following:
- Nil reply: The key didn't exist before this command (whether the key
was created or not).
- Bulk string reply: The previous value of the key (whether the key was
set or not).

### DELEX

```
DELEX key [IFEQ match-value | IFNE match-value | IFDEQ match-digest | IFDNE match-digest]
```

Conditionally removes the specified key. A key is ignored if it does not
exist.

`IFEQ match-value` - Delete the key only if its value is equal to
match-value
`IFNE match-value` - Delete the key only if its value is not equal to
match-value
`IFDEQ match-digest` - Delete the key only if the digest of its value is
equal to match-digest
`IFDNE match-digest` - Delete the key only if the digest of its value is
not equal to match-digest

Reply: 
- error if key exists but holds a value that is not a string and
IFEQ/IFNE/IFDEQ/IFDNE is specified.
- (integer) 0 if not deleted (the key does not exist or a specified
IFEQ/IFNE/IFDEQ/IFDNE condition is false), or 1 if deleted.

### Notes

Added copy of xxhash repo to deps -
[version](c961fbe61a)

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
Co-authored-by: Yuan Wang <wangyuancode@163.com>
This commit is contained in:
Mincho Paskalev 2025-10-21 10:32:49 +03:00 committed by GitHub
parent a01c061866
commit aed879ad0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 9775 additions and 32 deletions

7
deps/Makefile vendored
View file

@ -60,6 +60,7 @@ distclean:
-(cd hdr_histogram && $(MAKE) clean) > /dev/null || true
-(cd fpconv && $(MAKE) clean) > /dev/null || true
-(cd fast_float && $(MAKE) clean) > /dev/null || true
-(cd xxhash && $(MAKE) clean) > /dev/null || true
-(rm -f .make-*)
.PHONY: distclean
@ -100,6 +101,12 @@ fast_float: .make-prerequisites
.PHONY: fast_float
XXHASH_CFLAGS = -fPIC $(DEPS_CFLAGS)
xxhash: .make-prerequisites
@printf '%b %b\n' $(MAKECOLOR)MAKE$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR)
cd xxhash && $(MAKE) lib CFLAGS="$(XXHASH_CFLAGS)" LDFLAGS="$(DEPS_LDFLAGS)"
.PHONY: xxhash
ifeq ($(uname_S),SunOS)
# Make isinf() available
LUA_CFLAGS= -D__C99FEATURES__=1

6
deps/xxhash/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
# Ignore:
libxxhash.*
xxh*sum
# But include:
!libxxhash.pc.in

26
deps/xxhash/LICENSE vendored Normal file
View file

@ -0,0 +1,26 @@
xxHash Library
Copyright (c) 2012-2021 Yann Collet
All rights reserved.
BSD 2-Clause License (https://www.opensource.org/licenses/bsd-license.php)
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

687
deps/xxhash/Makefile vendored Normal file
View file

@ -0,0 +1,687 @@
# ################################################################
# xxHash Makefile
# Copyright (C) 2012-2024 Yann Collet
#
# GPL v2 License
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# You can contact the author at:
# - xxHash homepage: https://www.xxhash.com
# - xxHash source repository: https://github.com/Cyan4973/xxHash
# ################################################################
# xxhsum: provides 32/64 bits hash of one or multiple files, or stdin
# ################################################################
# Version numbers
SED ?= sed
SED_ERE_OPT ?= -E
LIBVER_MAJOR_SCRIPT:=`$(SED) -n '/define XXH_VERSION_MAJOR/s/.*[[:blank:]]\([0-9][0-9]*\).*/\1/p' < xxhash.h`
LIBVER_MINOR_SCRIPT:=`$(SED) -n '/define XXH_VERSION_MINOR/s/.*[[:blank:]]\([0-9][0-9]*\).*/\1/p' < xxhash.h`
LIBVER_PATCH_SCRIPT:=`$(SED) -n '/define XXH_VERSION_RELEASE/s/.*[[:blank:]]\([0-9][0-9]*\).*/\1/p' < xxhash.h`
LIBVER_MAJOR := $(shell echo $(LIBVER_MAJOR_SCRIPT))
LIBVER_MINOR := $(shell echo $(LIBVER_MINOR_SCRIPT))
LIBVER_PATCH := $(shell echo $(LIBVER_PATCH_SCRIPT))
LIBVER := $(LIBVER_MAJOR).$(LIBVER_MINOR).$(LIBVER_PATCH)
MAKEFLAGS += --no-print-directory
CFLAGS ?= -O3
DEBUGFLAGS+=-Wall -Wextra -Wconversion -Wcast-qual -Wcast-align -Wshadow \
-Wstrict-aliasing=1 -Wswitch-enum -Wdeclaration-after-statement \
-Wstrict-prototypes -Wundef -Wpointer-arith -Wformat-security \
-Wvla -Wformat=2 -Winit-self -Wfloat-equal -Wwrite-strings \
-Wredundant-decls -Wstrict-overflow=2
CFLAGS += $(DEBUGFLAGS) $(MOREFLAGS)
FLAGS = $(CFLAGS) $(CPPFLAGS)
XXHSUM_VERSION = $(LIBVER)
# Define *.exe as extension for Windows systems
ifneq (,$(filter Windows%,$(OS)))
EXT =.exe
else
EXT =
endif
# automatically enable runtime vector dispatch on x86/64 targets
detect_x86_arch = $(shell $(CC) -dumpmachine | grep -E 'i[3-6]86|x86_64')
ifneq ($(strip $(call detect_x86_arch)),)
#note: can be overridden at compile time, by setting DISPATCH=0
DISPATCH ?= 1
else
ifeq ($(DISPATCH),1)
$(info "Note: DISPATCH=1 is only supported on x86/x64 targets")
endif
override DISPATCH := 0
endif
ifeq ($(NODE_JS),1)
# Link in unrestricted filesystem support
LDFLAGS += -sNODERAWFS
# Set flag to fix isatty() support
CPPFLAGS += -DXSUM_NODE_JS=1
endif
# OS X linker doesn't support -soname, and use different extension
# see: https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/DynamicLibraryDesignGuidelines.html
UNAME ?= $(shell uname)
ifeq ($(UNAME), Darwin)
SHARED_EXT = dylib
SHARED_EXT_MAJOR = $(LIBVER_MAJOR).$(SHARED_EXT)
SHARED_EXT_VER = $(LIBVER).$(SHARED_EXT)
SONAME_FLAGS = -install_name $(LIBDIR)/libxxhash.$(SHARED_EXT_MAJOR) -compatibility_version $(LIBVER_MAJOR) -current_version $(LIBVER)
else
SONAME_FLAGS = -Wl,-soname=libxxhash.$(SHARED_EXT).$(LIBVER_MAJOR)
SHARED_EXT = so
SHARED_EXT_MAJOR = $(SHARED_EXT).$(LIBVER_MAJOR)
SHARED_EXT_VER = $(SHARED_EXT).$(LIBVER)
endif
LIBXXH = libxxhash.$(SHARED_EXT_VER)
CLI_DIR = cli
CLI_SRCS = $(wildcard $(CLI_DIR)/*.c)
CLI_OBJS = $(CLI_SRCS:.c=.o)
## define default before including multiconf.make
## generate CLI and libraries in release mode (default for `make`)
.PHONY: default
default: DEBUGFLAGS=
default: lib xxhsum_and_links
C_SRCDIRS = . $(CLI_DIR) fuzz
include build/make/multiconf.make
.PHONY: all
all: lib xxhsum xxhsum_inlinedXXH
## xxhsum is the command line interface (CLI)
ifeq ($(DISPATCH),1)
xxhsum: CPPFLAGS += -DXXHSUM_DISPATCH=1
XXHSUM_ADD_O = xxh_x86dispatch.o
endif
$(eval $(call c_program,xxhsum,xxhash.o $(CLI_OBJS) $(XXHSUM_ADD_O)))
.PHONY: xxhsum_and_links
xxhsum_and_links: xxhsum xxh32sum xxh64sum xxh128sum xxh3sum
LN ?= ln
xxh32sum xxh64sum xxh128sum xxh3sum: xxhsum
$(LN) -sf $<$(EXT) $@$(EXT)
## generate CLI in 32-bits mode
xxhsum32: CFLAGS += -m32
ifeq ($(DISPATCH),1)
xxhsum32: CPPFLAGS += -DXXHSUM_DISPATCH=1
endif
$(eval $(call c_program,xxhsum32,xxhash.o $(CLI_OBJS) $(XXHSUM_ADD_O)))
## Warning: dispatch only works for x86/x64 systems
dispatch: CPPFLAGS += -DXXHSUM_DISPATCH=1
$(eval $(call c_program,dispatch,xxhash.o xxh_x86dispatch.o $(CLI_OBJS)))
xxhsum_inlinedXXH: CPPFLAGS += -DXXH_INLINE_ALL
$(eval $(call c_program,xxhsum_inlinedXXH,$(CLI_OBJS)))
# =================================================
# library
libxxhash.a:
$(eval $(call static_library,libxxhash.a,xxhash.o))
$(LIBXXH): LDFLAGS += $(SONAME_FLAGS)
ifeq (,$(filter Windows%,$(OS)))
$(LIBXXH): CFLAGS += -fPIC
endif
LIBXXHASH_OBJS := xxhash.o $(if $(filter 1,$(LIBXXH_DISPATCH)),xxh_x86dispatch.o)
$(eval $(call c_dynamic_library,$(LIBXXH),$(LIBXXHASH_OBJS)))
libxxhash.$(SHARED_EXT_MAJOR): $(LIBXXH)
$(LN) -sf $< $@
libxxhash.$(SHARED_EXT): libxxhash.$(SHARED_EXT_MAJOR)
$(LN) -sf $< $@
.PHONY: libxxhash ## generate dynamic xxhash library
libxxhash: $(LIBXXH) libxxhash.$(SHARED_EXT_MAJOR) libxxhash.$(SHARED_EXT)
.PHONY: lib ## generate static and dynamic xxhash libraries
lib: libxxhash.a libxxhash
# helper targets
AWK ?= awk
GREP ?= grep
SORT ?= sort
NM ?= nm
.PHONY: list
list: ## list all Makefile targets
$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | $(AWK) -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | $(SORT) | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | xargs
.PHONY: help
help: ## list documented targets
$(GREP) -E '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
$(SORT) | \
$(AWK) 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
.PHONY: clean
clean:
$(RM) -r *.dSYM # Mac OS-X specific
$(RM) core *.o *.obj *.$(SHARED_EXT) *.$(SHARED_EXT).* *.a libxxhash.pc
$(RM) xxhsum.wasm xxhsum.js xxhsum.html
$(RM) xxh32sum$(EXT) xxh64sum$(EXT) xxh128sum$(EXT) xxh3sum$(EXT)
$(RM) fuzzer
$(MAKE) -C tests clean
$(MAKE) -C tests/bench clean
$(MAKE) -C tests/collisions clean
@echo cleaning completed
# =================================================
# tests
# =================================================
# make check can be run with cross-compiled binaries on emulated environments (qemu user mode)
# by setting $(RUN_ENV) to the target emulation environment
.PHONY: check
check: xxhsum test_sanity ## basic tests for xxhsum CLI, set RUN_ENV for emulated environments
# stdin
# If you get "Wrong parameters" on Emscripten+Node.js, recompile with `NODE_JS=1`
$(RUN_ENV) ./xxhsum$(EXT) < xxhash.c
# multiple files
$(RUN_ENV) ./xxhsum$(EXT) xxhash.*
# internal bench
$(RUN_ENV) ./xxhsum$(EXT) -bi0
# long bench command
$(RUN_ENV) ./xxhsum$(EXT) --benchmark-all -i0
# bench multiple variants
$(RUN_ENV) ./xxhsum$(EXT) -b1,2,3 -i0
# file bench
$(RUN_ENV) ./xxhsum$(EXT) -bi0 xxhash.c
# 32-bit
$(RUN_ENV) ./xxhsum$(EXT) -H0 xxhash.c
# 128-bit
$(RUN_ENV) ./xxhsum$(EXT) -H2 xxhash.c
# XXH3 (enforce BSD style)
$(RUN_ENV) ./xxhsum$(EXT) -H3 xxhash.c | grep "XXH3"
# request incorrect variant
$(RUN_ENV) ./xxhsum$(EXT) -H9 xxhash.c ; test $$? -eq 1
@printf "\n ....... checks completed successfully ....... \n"
.PHONY: test-unicode
test-unicode:
$(MAKE) -C tests test_unicode
.PHONY: test_sanity
test_sanity:
$(MAKE) -C tests test_sanity
.PHONY: test-mem
VALGRIND = valgrind --leak-check=yes --error-exitcode=1
test-mem: RUN_ENV = $(VALGRIND)
test-mem: xxhsum check
.PHONY: test32
test32: xxhsum32
@echo ---- test 32-bit ----
./xxhsum32 -bi0 xxhash.c
TEST_FILES = xxhsum$(EXT) xxhash.c xxhash.h
.PHONY: test-xxhsum-c
test-xxhsum-c: xxhsum
# xxhsum to/from pipe
./xxhsum $(TEST_FILES) | ./xxhsum -c -
./xxhsum -H0 $(TEST_FILES) | ./xxhsum -c -
# xxhsum -c is unable to verify checksum of file from STDIN (#470)
./xxhsum < README.md > .test.README.md.xxh
./xxhsum -c .test.README.md.xxh < README.md
# xxhsum -q does not display "Loading" message into stderr (#251)
! ./xxhsum -q $(TEST_FILES) 2>&1 | grep Loading
# xxhsum does not display "Loading" message into stderr either
! ./xxhsum $(TEST_FILES) 2>&1 | grep Loading
# Check that xxhsum do display filename that it failed to open.
LC_ALL=C ./xxhsum nonexistent 2>&1 | grep "Error: Could not open 'nonexistent'"
# xxhsum to/from file, shell redirection
./xxhsum $(TEST_FILES) > .test.xxh64
./xxhsum --tag $(TEST_FILES) > .test.xxh64_tag
./xxhsum --little-endian $(TEST_FILES) > .test.le_xxh64
./xxhsum --tag --little-endian $(TEST_FILES) > .test.le_xxh64_tag
./xxhsum -H0 $(TEST_FILES) > .test.xxh32
./xxhsum -H0 --tag $(TEST_FILES) > .test.xxh32_tag
./xxhsum -H0 --little-endian $(TEST_FILES) > .test.le_xxh32
./xxhsum -H0 --tag --little-endian $(TEST_FILES) > .test.le_xxh32_tag
./xxhsum -H2 $(TEST_FILES) > .test.xxh128
./xxhsum -H2 --tag $(TEST_FILES) > .test.xxh128_tag
./xxhsum -H2 --little-endian $(TEST_FILES) > .test.le_xxh128
./xxhsum -H2 --tag --little-endian $(TEST_FILES) > .test.le_xxh128_tag
./xxhsum -H3 $(TEST_FILES) > .test.xxh3
./xxhsum -H3 --tag $(TEST_FILES) > .test.xxh3_tag
./xxhsum -H3 --little-endian $(TEST_FILES) > .test.le_xxh3
./xxhsum -H3 --tag --little-endian $(TEST_FILES) > .test.le_xxh3_tag
./xxhsum -c .test.xxh*
./xxhsum -c --little-endian .test.le_xxh*
./xxhsum -c .test.*_tag
# read list of files from stdin
./xxhsum -c < .test.xxh32
./xxhsum -c < .test.xxh64
./xxhsum -c < .test.xxh128
./xxhsum -c < .test.xxh3
cat .test.xxh* | ./xxhsum -c -
# check variant with '*' marker as second separator
$(SED) 's/ / \*/' .test.xxh32 | ./xxhsum -c
# bsd-style output
./xxhsum --tag xxhsum* | $(GREP) XXH64
./xxhsum --tag -H0 xxhsum* | $(GREP) XXH32
./xxhsum --tag -H1 xxhsum* | $(GREP) XXH64
./xxhsum --tag -H2 xxhsum* | $(GREP) XXH128
./xxhsum --tag -H3 xxhsum* | $(GREP) XXH3
./xxhsum -H3 xxhsum* | $(GREP) XXH3_ # prefix for GNU format
./xxhsum --tag -H32 xxhsum* | $(GREP) XXH32
./xxhsum --tag -H64 xxhsum* | $(GREP) XXH64
./xxhsum --tag -H128 xxhsum* | $(GREP) XXH128
./xxhsum --tag -H0 --little-endian xxhsum* | $(GREP) XXH32_LE
./xxhsum --tag -H1 --little-endian xxhsum* | $(GREP) XXH64_LE
./xxhsum --tag -H2 --little-endian xxhsum* | $(GREP) XXH128_LE
./xxhsum --tag -H3 --little-endian xxhsum* | $(GREP) XXH3_LE
./xxhsum --tag -H32 --little-endian xxhsum* | $(GREP) XXH32_LE
./xxhsum --tag -H64 --little-endian xxhsum* | $(GREP) XXH64_LE
./xxhsum --tag -H128 --little-endian xxhsum* | $(GREP) XXH128_LE
# check bsd-style
./xxhsum --tag xxhsum* | ./xxhsum -c
./xxhsum --tag -H32 --little-endian xxhsum* | ./xxhsum -c
# xxhsum -c warns improperly format lines.
echo '12345678 ' >>.test.xxh32
./xxhsum -c .test.xxh32 | $(GREP) improperly
echo '123456789 file' >>.test.xxh64
./xxhsum -c .test.xxh64 | $(GREP) improperly
# Expects "FAILED"
echo "0000000000000000 LICENSE" | ./xxhsum -c -; test $$? -eq 1
echo "00000000 LICENSE" | ./xxhsum -c -; test $$? -eq 1
# Expects "FAILED open or read"
echo "0000000000000000 test-expects-file-not-found" | ./xxhsum -c -; test $$? -eq 1
echo "00000000 test-expects-file-not-found" | ./xxhsum -c -; test $$? -eq 1
# --filelist
echo xxhash.c > .test.filenames
$(RUN_ENV) ./xxhsum$(EXT) --filelist .test.filenames
# --filelist from stdin
cat .test.filenames | $(RUN_ENV) ./xxhsum$(EXT) --filelist
@$(RM) .test.*
CC_VERSION := $(shell $(CC) --version 2>/dev/null)
ifneq (,$(findstring clang,$(CC_VERSION)))
fuzzer: CFLAGS += -fsanitize=fuzzer
$(eval $(call c_program,fuzzer, fuzz/fuzzer.o xxhash.o))
else
fuzzer: this_target_requires_clang # intentional fail
endif
.PHONY: test-filename-escape
test-filename-escape:
$(MAKE) -C tests test_filename_escape
.PHONY: test-cli-comment-line
test-cli-comment-line:
$(MAKE) -C tests test_cli_comment_line
.PHONY: test-cli-ignore-missing
test-cli-ignore-missing:
$(MAKE) -C tests test_cli_ignore_missing
.PHONY: armtest
armtest:
@echo ---- test ARM compilation ----
CC=arm-linux-gnueabi-gcc MOREFLAGS="-Werror -static" $(MAKE) xxhsum
.PHONY: arm64test
arm64test:
@echo ---- test ARM64 compilation ----
CC=aarch64-linux-gnu-gcc MOREFLAGS="-Werror -static" $(MAKE) xxhsum
.PHONY: clangtest
clangtest:
@echo ---- test clang compilation ----
CC=clang MOREFLAGS="-Werror -Wconversion -Wno-sign-conversion" $(MAKE) all
.PHONY: gcc-og-test
gcc-og-test:
@echo ---- test gcc -Og compilation ----
CFLAGS="-Og -Wall -Wextra -Wundef -Wshadow -Wcast-align -Werror -fPIC" CPPFLAGS="-DXXH_NO_INLINE_HINTS" MOREFLAGS="-Werror" $(MAKE) all
.PHONY: cxxtest
cxxtest:
@echo ---- test C++ compilation ----
CC="$(CXX) -Wno-deprecated" $(MAKE) all CFLAGS="-O3 -Wall -Wextra -Wundef -Wshadow -Wcast-align -Werror -fPIC"
# In strict C90 mode, there is no `long long` type support,
# consequently, only XXH32 can be compiled.
.PHONY: c90test
ifeq ($(NO_C90_TEST),true)
c90test:
@echo no c90 compatibility test
else
c90test: CPPFLAGS += -DXXH_NO_LONG_LONG
c90test: CFLAGS += -std=c90 -Werror -pedantic
c90test: xxhash.c
@echo ---- test strict C90 compilation [xxh32 only] ----
$(RM) xxhash.o
$(CC) $(FLAGS) $^ -c
$(NM) xxhash.o | $(GREP) XXH64 ; test $$? -eq 1
$(RM) xxhash.o
endif
.PHONY: noxxh3test
noxxh3test: CPPFLAGS += -DXXH_NO_XXH3
noxxh3test: CFLAGS += -Werror -pedantic -Wno-long-long # XXH64 requires long long support
noxxh3test: OFILE = xxh_noxxh3.o
noxxh3test: xxhash.c
@echo ---- test compilation without XXH3 ----
$(CC) $(FLAGS) -c $^ -o $(OFILE)
$(NM) $(OFILE) | $(GREP) XXH3_ ; test $$? -eq 1
$(RM) $(OFILE)
.PHONY: nostreamtest
nostreamtest: CPPFLAGS += -DXXH_NO_STREAM
nostreamtest: CFLAGS += -Werror -pedantic -Wno-long-long # XXH64 requires long long support
nostreamtest: OFILE = xxh_nostream.o
nostreamtest: xxhash.c
@echo ---- test compilation without streaming ----
$(CC) $(FLAGS) -c $^ -o $(OFILE)
$(NM) $(OFILE) | $(GREP) update ; test $$? -eq 1
$(RM) $(OFILE)
.PHONY: nostdlibtest
nostdlibtest: CPPFLAGS += -DXXH_NO_STDLIB
nostdlibtest: CFLAGS += -Werror -pedantic -Wno-long-long # XXH64 requires long long support
nostdlibtest: OFILE = xxh_nostdlib.o
nostdlibtest: xxhash.c
@echo ---- test compilation without \<stdlib.h\> ----
$(CC) $(FLAGS) -c $^ -o $(OFILE)
$(NM) $(OFILE) | $(GREP) "U _free\|U free" ; test $$? -eq 1
$(RM) $(OFILE)
.PHONY: usan
usan: CC=clang
usan: CXX=clang++
usan: ## check CLI runtime for undefined behavior, using clang's sanitizer
@echo ---- check undefined behavior - sanitize ----
$(MAKE) test CC=$(CC) CXX=$(CXX) MOREFLAGS="-g -fsanitize=undefined -fno-sanitize-recover=all"
.PHONY: staticAnalyze
SCANBUILD ?= scan-build
staticAnalyze: clean ## check C source files using $(SCANBUILD) static analyzer
@echo ---- static analyzer - $(SCANBUILD) ----
CFLAGS="-g -Werror" $(SCANBUILD) --status-bugs -v $(MAKE) all
CPPCHECK ?= cppcheck
.PHONY: cppcheck
cppcheck: ## check C source files using $(CPPCHECK) static analyzer
@echo ---- static analyzer - $(CPPCHECK) ----
$(CPPCHECK) . --force --enable=warning,portability,performance,style --error-exitcode=1 > /dev/null
.PHONY: namespaceTest
namespaceTest: ## ensure XXH_NAMESPACE redefines all public symbols
$(CC) -c xxhash.c
$(CC) -DXXH_NAMESPACE=TEST_ -c xxhash.c -o xxhash2.o
$(CC) xxhash.o xxhash2.o $(CLI_SRCS) -o xxhsum2 # will fail if one namespace missing (symbol collision)
$(RM) *.o xxhsum2 # clean
MAN = $(CLI_DIR)/xxhsum.1
MD2ROFF ?= ronn
MD2ROFF_FLAGS ?= --roff --warnings --manual="User Commands" --organization="xxhsum $(XXHSUM_VERSION)"
$(MAN): $(CLI_DIR)/xxhsum.1.md xxhash.h
cat $< | $(MD2ROFF) $(MD2ROFF_FLAGS) | $(SED) -n '/^\.\\\".*/!p' > $@
.PHONY: man
man: $(MAN) ## generate man page from markdown source
.PHONY: clean-man
clean-man:
$(RM) xxhsum.1
.PHONY: preview-man
preview-man: man
man ./xxhsum.1
.PHONY: test
test: DEBUGFLAGS += -DXXH_DEBUGLEVEL=1
test: all namespaceTest check test-xxhsum-c c90test test-tools noxxh3test nostdlibtest
# this test checks that including "xxhash.h" multiple times and with different directives still compiles properly
.PHONY: test-multiInclude
test-multiInclude:
$(MAKE) -C tests test_multiInclude
.PHONY: test-inline-notexposed
test-inline-notexposed: xxhsum_inlinedXXH
$(NM) xxhsum_inlinedXXH | $(GREP) "t _XXH32_" ; test $$? -eq 1 # no XXH32 symbol should be left
$(NM) xxhsum_inlinedXXH | $(GREP) "t _XXH64_" ; test $$? -eq 1 # no XXH64 symbol should be left
.PHONY: test-inline
test-inline: test-inline-notexposed test-multiInclude
.PHONY: test-all
test-all: CFLAGS += -Werror
test-all: test test32 test-unicode clangtest gcc-og-test cxxtest usan test-inline listL120 trailingWhitespace test-xxh-nnn-sums
.PHONY: test-tools
test-tools:
CFLAGS=-Werror $(MAKE) -C tests/bench
CFLAGS=-Werror $(MAKE) -C tests/collisions check
.PHONY: test-xxh-nnn-sums
test-xxh-nnn-sums: xxhsum_and_links
./xxhsum README.md > tmp.xxhsum.out # xxhsum outputs xxh64
./xxh32sum README.md > tmp.xxh32sum.out
./xxh64sum README.md > tmp.xxh64sum.out
./xxh128sum README.md > tmp.xxh128sum.out
./xxh3sum README.md > tmp.xxh3sum.out
cat tmp.xxhsum.out
cat tmp.xxh32sum.out
cat tmp.xxh64sum.out
cat tmp.xxh128sum.out
cat tmp.xxh3sum.out
./xxhsum -c tmp.xxhsum.out
./xxhsum -c tmp.xxh32sum.out
./xxhsum -c tmp.xxh64sum.out
./xxhsum -c tmp.xxh128sum.out
./xxhsum -c tmp.xxh3sum.out
./xxh32sum -c tmp.xxhsum.out ; test $$? -eq 1 # expects "no properly formatted"
./xxh32sum -c tmp.xxh32sum.out
./xxh32sum -c tmp.xxh64sum.out ; test $$? -eq 1 # expects "no properly formatted"
./xxh32sum -c tmp.xxh128sum.out ; test $$? -eq 1 # expects "no properly formatted"
./xxh32sum -c tmp.xxh3sum.out ; test $$? -eq 1 # expects "no properly formatted"
./xxh64sum -c tmp.xxhsum.out
./xxh64sum -c tmp.xxh32sum.out ; test $$? -eq 1 # expects "no properly formatted"
./xxh64sum -c tmp.xxh64sum.out
./xxh64sum -c tmp.xxh128sum.out ; test $$? -eq 1 # expects "no properly formatted"
./xxh64sum -c tmp.xxh3sum.out ; test $$? -eq 1 # expects "no properly formatted"
./xxh128sum -c tmp.xxhsum.out ; test $$? -eq 1 # expects "no properly formatted"
./xxh128sum -c tmp.xxh32sum.out ; test $$? -eq 1 # expects "no properly formatted"
./xxh128sum -c tmp.xxh64sum.out ; test $$? -eq 1 # expects "no properly formatted"
./xxh128sum -c tmp.xxh128sum.out
./xxh128sum -c tmp.xxh3sum.out ; test $$? -eq 1 # expects "no properly formatted"
./xxh3sum -c tmp.xxhsum.out ; test $$? -eq 1 # expects "no properly formatted"
./xxh3sum -c tmp.xxh32sum.out ; test $$? -eq 1 # expects "no properly formatted"
./xxh3sum -c tmp.xxh64sum.out ; test $$? -eq 1 # expects "no properly formatted"
./xxh3sum -c tmp.xxh128sum.out ; test $$? -eq 1 # expects "no properly formatted"
./xxh3sum -c tmp.xxh3sum.out
.PHONY: listL120
listL120: # extract lines >= 120 characters in *.{c,h}, by Takayuki Matsuoka (note: $$, for Makefile compatibility)
find . -type f -name '*.c' -o -name '*.h' | while read -r filename; do awk 'length > 120 {print FILENAME "(" FNR "): " $$0}' $$filename; done
.PHONY: trailingWhitespace
trailingWhitespace:
! $(GREP) -E "`printf '[ \\t]$$'`" cli/*.c cli/*.h cli/*.1 *.c *.h LICENSE Makefile build/cmake/CMakeLists.txt
.PHONY: lint-unicode
lint-unicode:
./tests/unicode_lint.sh
# =========================================================
# make install is validated only for the following targets
# =========================================================
ifneq (,$(filter Linux Darwin GNU/kFreeBSD GNU Haiku OpenBSD FreeBSD NetBSD DragonFly SunOS CYGWIN% , $(UNAME)))
DESTDIR ?=
# directory variables: GNU conventions prefer lowercase
# see https://www.gnu.org/prep/standards/html_node/Makefile-Conventions.html
# support both lower and uppercase (BSD), use uppercase in script
prefix ?= /usr/local
PREFIX ?= $(prefix)
exec_prefix ?= $(PREFIX)
EXEC_PREFIX ?= $(exec_prefix)
libdir ?= $(EXEC_PREFIX)/lib
LIBDIR ?= $(libdir)
includedir ?= $(PREFIX)/include
INCLUDEDIR ?= $(includedir)
bindir ?= $(EXEC_PREFIX)/bin
BINDIR ?= $(bindir)
datarootdir ?= $(PREFIX)/share
mandir ?= $(datarootdir)/man
man1dir ?= $(mandir)/man1
ifneq (,$(filter $(UNAME),FreeBSD NetBSD DragonFly))
PKGCONFIGDIR ?= $(PREFIX)/libdata/pkgconfig
else
PKGCONFIGDIR ?= $(LIBDIR)/pkgconfig
endif
ifneq (,$(filter $(UNAME),OpenBSD NetBSD DragonFly SunOS))
MANDIR ?= $(PREFIX)/man/man1
else
MANDIR ?= $(man1dir)
endif
ifneq (,$(filter $(UNAME),SunOS))
INSTALL ?= ginstall
else
INSTALL ?= install
endif
INSTALL_PROGRAM ?= $(INSTALL)
INSTALL_DATA ?= $(INSTALL) -m 644
MAKE_DIR ?= $(INSTALL) -d -m 755
# Escape special symbols by putting each character into its separate class
EXEC_PREFIX_REGEX ?= $(shell echo "$(EXEC_PREFIX)" | $(SED) $(SED_ERE_OPT) -e "s/([^^])/[\1]/g" -e "s/\\^/\\\\^/g")
PREFIX_REGEX ?= $(shell echo "$(PREFIX)" | $(SED) $(SED_ERE_OPT) -e "s/([^^])/[\1]/g" -e "s/\\^/\\\\^/g")
PCLIBDIR ?= $(shell echo "$(LIBDIR)" | $(SED) -n $(SED_ERE_OPT) -e "s@^$(EXEC_PREFIX_REGEX)(/|$$)@@p")
PCINCDIR ?= $(shell echo "$(INCLUDEDIR)" | $(SED) -n $(SED_ERE_OPT) -e "s@^$(PREFIX_REGEX)(/|$$)@@p")
PCEXECDIR?= $(if $(filter $(PREFIX),$(EXEC_PREFIX)),$$\{prefix\},$(EXEC_PREFIX))
ifeq (,$(PCLIBDIR))
# Additional prefix check is required, since the empty string is technically a
# valid PCLIBDIR
ifeq (,$(shell echo "$(LIBDIR)" | $(SED) -n $(SED_ERE_OPT) -e "\\@^$(EXEC_PREFIX_REGEX)(/|$$)@ p"))
$(error configured libdir ($(LIBDIR)) is outside of exec_prefix ($(EXEC_PREFIX)), can't generate pkg-config file)
endif
endif
ifeq (,$(PCINCDIR))
# Additional prefix check is required, since the empty string is technically a
# valid PCINCDIR
ifeq (,$(shell echo "$(INCLUDEDIR)" | $(SED) -n $(SED_ERE_OPT) -e "\\@^$(PREFIX_REGEX)(/|$$)@ p"))
$(error configured includedir ($(INCLUDEDIR)) is outside of prefix ($(PREFIX)), can't generate pkg-config file)
endif
endif
libxxhash.pc: libxxhash.pc.in
@echo creating pkgconfig
$(SED) $(SED_ERE_OPT) -e 's|@PREFIX@|$(PREFIX)|' \
-e 's|@EXECPREFIX@|$(PCEXECDIR)|' \
-e 's|@LIBDIR@|$$\{exec_prefix\}/$(PCLIBDIR)|' \
-e 's|@INCLUDEDIR@|$$\{prefix\}/$(PCINCDIR)|' \
-e 's|@VERSION@|$(LIBVER)|' \
$< > $@
install_libxxhash.a: libxxhash.a
@echo Installing libxxhash.a
$(MAKE_DIR) $(DESTDIR)$(LIBDIR)
$(INSTALL_DATA) libxxhash.a $(DESTDIR)$(LIBDIR)
install_libxxhash: libxxhash
@echo Installing libxxhash
$(MAKE_DIR) $(DESTDIR)$(LIBDIR)
$(INSTALL_PROGRAM) $(LIBXXH) $(DESTDIR)$(LIBDIR)
ln -sf $(LIBXXH) $(DESTDIR)$(LIBDIR)/libxxhash.$(SHARED_EXT_MAJOR)
ln -sf libxxhash.$(SHARED_EXT_MAJOR) $(DESTDIR)$(LIBDIR)/libxxhash.$(SHARED_EXT)
install_libxxhash.includes:
$(INSTALL) -d -m 755 $(DESTDIR)$(INCLUDEDIR) # includes
$(INSTALL_DATA) xxhash.h $(DESTDIR)$(INCLUDEDIR)
$(INSTALL_DATA) xxh3.h $(DESTDIR)$(INCLUDEDIR) # for compatibility, will be removed in v0.9.0
ifeq ($(LIBXXH_DISPATCH),1)
$(INSTALL_DATA) xxh_x86dispatch.h $(DESTDIR)$(INCLUDEDIR)
endif
install_libxxhash.pc: libxxhash.pc
@echo Installing pkgconfig
$(MAKE_DIR) $(DESTDIR)$(PKGCONFIGDIR)/
$(INSTALL_DATA) libxxhash.pc $(DESTDIR)$(PKGCONFIGDIR)/
install_xxhsum: xxhsum
@echo Installing xxhsum
$(MAKE_DIR) $(DESTDIR)$(BINDIR)/
$(INSTALL_PROGRAM) xxhsum$(EXT) $(DESTDIR)$(BINDIR)/xxhsum$(EXT)
ln -sf xxhsum$(EXT) $(DESTDIR)$(BINDIR)/xxh32sum$(EXT)
ln -sf xxhsum$(EXT) $(DESTDIR)$(BINDIR)/xxh64sum$(EXT)
ln -sf xxhsum$(EXT) $(DESTDIR)$(BINDIR)/xxh128sum$(EXT)
ln -sf xxhsum$(EXT) $(DESTDIR)$(BINDIR)/xxh3sum$(EXT)
install_man:
@echo Installing man pages
$(MAKE_DIR) $(DESTDIR)$(MANDIR)/
$(INSTALL_DATA) $(MAN) $(DESTDIR)$(MANDIR)/xxhsum.1
ln -sf xxhsum.1 $(DESTDIR)$(MANDIR)/xxh32sum.1
ln -sf xxhsum.1 $(DESTDIR)$(MANDIR)/xxh64sum.1
ln -sf xxhsum.1 $(DESTDIR)$(MANDIR)/xxh128sum.1
ln -sf xxhsum.1 $(DESTDIR)$(MANDIR)/xxh3sum.1
.PHONY: install
## install libraries, CLI, links and man pages
install: install_libxxhash.a install_libxxhash install_libxxhash.includes install_libxxhash.pc install_xxhsum install_man
@echo xxhash installation completed
.PHONY: uninstall
uninstall: ## uninstall libraries, CLI, links and man page
$(RM) $(DESTDIR)$(LIBDIR)/libxxhash.a
$(RM) $(DESTDIR)$(LIBDIR)/libxxhash.$(SHARED_EXT)
$(RM) $(DESTDIR)$(LIBDIR)/libxxhash.$(SHARED_EXT_MAJOR)
$(RM) $(DESTDIR)$(LIBDIR)/$(LIBXXH)
$(RM) $(DESTDIR)$(INCLUDEDIR)/xxhash.h
$(RM) $(DESTDIR)$(INCLUDEDIR)/xxh3.h
$(RM) $(DESTDIR)$(INCLUDEDIR)/xxh_x86dispatch.h
$(RM) $(DESTDIR)$(PKGCONFIGDIR)/libxxhash.pc
$(RM) $(DESTDIR)$(BINDIR)/xxh32sum
$(RM) $(DESTDIR)$(BINDIR)/xxh64sum
$(RM) $(DESTDIR)$(BINDIR)/xxh128sum
$(RM) $(DESTDIR)$(BINDIR)/xxh3sum
$(RM) $(DESTDIR)$(BINDIR)/xxhsum
$(RM) $(DESTDIR)$(MANDIR)/xxh32sum.1
$(RM) $(DESTDIR)$(MANDIR)/xxh64sum.1
$(RM) $(DESTDIR)$(MANDIR)/xxh128sum.1
$(RM) $(DESTDIR)$(MANDIR)/xxh3sum.1
$(RM) $(DESTDIR)$(MANDIR)/xxhsum.1
@echo xxhsum successfully uninstalled
endif

71
deps/xxhash/build/make/README.md vendored Normal file
View file

@ -0,0 +1,71 @@
# multiconf.make
**multiconf.make** is a self-contained Makefile include that lets you build the **same targets under many different flag sets**. For example debug vs release, ASan vs UBSan, GCC vs Clang.
Each different set of flags generates object files into its own **dedicated cache directory**, so objects compiled with one configuration are never reused by another.
Object files from previous configurations are preserved, so swapping back to a previous configuration only requires compiling objects which have actually changed.
---
## Benefits at a glance
| Benefit | What `multiconf.make` does |
| --- | --- |
| **Isolated configs** | Stores objects into `cachedObjs/<hash>/`, one directory per unique flag set. |
| **Fast switching** | Reusing an old config is instant—link only, no recompilation. |
| **Header deps** | Edits to headers trigger only needed rebuilds. |
| **One-liner targets** | Macros (`c_program`, `cxx_program`, …) hide all rule boilerplate. |
| **Parallel-ready** | Safe with `make -j`, no duplicate compiles of shared sources. |
| **Controlled verbosity** | Default only lists objects, while `V=1` display full commands. |
| **`clean` included** | `make clean` deletes all objects, binaries and links. |
---
## Quick Start
### 1 · List your sources
```make
C_SRCDIRS := src src/cdeps # all .c are in these directories
CXX_SRCDIRS := src src/cxxdeps # all .cpp are in these directories
```
### 2 · Add and include
```make
# root/Makefile
include multiconf.make
```
### 3 · Declare targets
```make
app:
$(eval $(call c_program,app,app.o cdeps/obj.o))
test:
$(eval $(call cxx_program,test, test.o cxxdeps/objcxx.o))
lib.a:
$(eval $(call static_library,lib.a, lib.o cdeps/obj.o))
lib.so:
$(eval $(call c_dynamic_library,lib.so, lib.o cdeps/obj.o))
```
### 4 · Build any config you like
```sh
# Release with GCC
make CFLAGS="-O3"
# Debug with Clang + AddressSanitizer (new cache dir)
make CC=clang CFLAGS="-g -O0 -fsanitize=address"
# Switch back to GCC release (objects still valid, relink only)
make CFLAGS="-O3"
```
Objects for each command live in different sub-folders; nothing overlaps.
---

293
deps/xxhash/build/make/multiconf.make vendored Normal file
View file

@ -0,0 +1,293 @@
# ##########################################################################
# multiconf.make
# Copyright (C) Yann Collet
#
# GPL v2 License
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# ##########################################################################
# Provides c_program(_shared_o) and cxx_program(_shared_o) target generation macros
# Provides static_library and c_dynamic_library target generation macros
# Support recompilation of only impacted units when an associated *.h is updated.
# Provides V=1 / VERBOSE=1 support. V=2 is used for debugging purposes.
# Complement target clean: delete objects and binaries created by this script
# Requires:
# - C_SRCDIRS, CXX_SRCDIRS, ASM_SRCDIRS defined
# OR
# C_SRCS, CXX_SRCS and ASM_SRCS variables defined
# *and* vpath set to find all source files
# OR
# C_OBJS, CXX_OBJS and ASM_OBJS variables defined
# *and* vpath set to find all source files
# - directory `cachedObjs/` available to cache object files.
# alternatively: set CACHE_ROOT to some different value.
# Optional:
# - HASH can be set to a different custom hash program.
# *_program*: generates a recipe for a target that will be built in a cache directory.
# The cache directory is automatically derived from CACHE_ROOT and list of flags and compilers.
# *_shared_o* variants are optional optimization variants, that share the same objects across multiple targets.
# However, as a consequence, all these objects must have exactly the same list of flags,
# which in practice means that there must be no target-level modification (like: target: CFLAGS += someFlag).
# If unsure, only use the standard variants, c_program and cxx_program.
# All *_program* macro functions take up to 4 argument:
# - The name of the target
# - The list of object files to build in the cache directory
# - An optional list of dependencies for linking, that will not be built
# - An optional complementary recipe code, that will run after compilation and link
# Silent mode is default; use V = 1 or VERBOSE = 1 to see compilation lines
VERBOSE ?= $(V)
$(VERBOSE).SILENT:
# Directory where object files will be built
CACHE_ROOT ?= cachedObjs
# --------------------------------------------------------------------------------------------
# Dependency management
DEPFLAGS = -MT $@ -MMD -MP -MF
# Include dependency files
include $(wildcard $(CACHE_ROOT)/**/*.d)
include $(wildcard $(CACHE_ROOT)/generic/*/*.d)
# --------------------------------------------------------------------------------------------
# Automatic determination of build artifacts cache directory, keyed on build
# flags, so that we can do incremental, parallel builds of different binaries
# with different build flags without collisions.
UNAME ?= $(shell uname)
ifeq ($(UNAME), Darwin)
HASH ?= md5
else ifeq ($(UNAME), FreeBSD)
HASH ?= gmd5sum
else ifeq ($(UNAME), OpenBSD)
HASH ?= md5
endif
HASH ?= md5sum
HAVE_HASH := $(shell echo 1 | $(HASH) > /dev/null && echo 1 || echo 0)
ifeq ($(HAVE_HASH),0)
$(info warning : could not find HASH ($(HASH)), required to differentiate builds using different flags)
HASH_FUNC = generic/$(1)
else
HASH_FUNC = $(firstword $(shell echo $(2) | $(HASH) ))
endif
MKDIR ?= mkdir
LN ?= ln
# --------------------------------------------------------------------------------------------
# The following macros are used to create object files in the cache directory.
# The object files are named after the source file, but with a different path.
# Create build directories on-demand.
#
# For some reason, make treats the directory as an intermediate file and tries
# to delete it. So we work around that by marking it "precious". Solution found
# here:
# http://ismail.badawi.io/blog/2017/03/28/automatic-directory-creation-in-make/
.PRECIOUS: $(CACHE_ROOT)/%/.
$(CACHE_ROOT)/%/. :
$(MKDIR) -p $@
define addTargetAsmObject # targetName, addlDeps
$$(if $$(filter 2,$$(V)),$$(info $$(call $(0),$(1),$(2))))
.PRECIOUS: $$(CACHE_ROOT)/%/$(1)
$$(CACHE_ROOT)/%/$(1) : $(1:.o=.S) $(2) | $$(CACHE_ROOT)/%/.
@echo AS $$@
$$(CC) $$(CPPFLAGS) $$(CXXFLAGS) $$(DEPFLAGS) $$(CACHE_ROOT)/$$*/$(1:.o=.d) -c $$< -o $$@
endef # addTargetAsmObject
define addTargetCObject # targetName, addlDeps
$$(if $$(filter 2,$$(V)),$$(info $$(call $(0),$(1),$(2)))) #debug print
.PRECIOUS: $$(CACHE_ROOT)/%/$(1)
$$(CACHE_ROOT)/%/$(1) : $(1:.o=.c) $(2) | $$(CACHE_ROOT)/%/.
@echo CC $$@
$$(CC) $$(CPPFLAGS) $$(CFLAGS) $$(DEPFLAGS) $$(CACHE_ROOT)/$$*/$(1:.o=.d) -c $$< -o $$@
endef # addTargetCObject
define addTargetCxxObject # targetName, suffix, addlDeps
$$(if $$(filter 2,$$(V)),$$(info $$(call $(0),$(1),$(2),$(3))))
.PRECIOUS: $$(CACHE_ROOT)/%/$(1)
$$(CACHE_ROOT)/%/$(1) : $(1:.o=.$(2)) $(3) | $$(CACHE_ROOT)/%/.
@echo CXX $$@
$$(CXX) $$(CPPFLAGS) $$(CXXFLAGS) $$(DEPFLAGS) $$(CACHE_ROOT)/$$*/$(1:.o=.d) -c $$< -o $$@
endef # addTargetCxxObject
# Create targets for individual object files
C_SRCDIRS += .
vpath %.c $(C_SRCDIRS)
CXX_SRCDIRS += .
vpath %.cpp $(CXX_SRCDIRS)
vpath %.cc $(CXX_SRCDIRS)
ASM_SRCDIRS += .
vpath %.S $(ASM_SRCDIRS)
# If C_SRCDIRS, CXX_SRCDIRS and ASM_SRCDIRS are not defined, use C_SRCS, CXX_SRCS and ASM_SRCS
C_SRCS ?= $(notdir $(foreach dir,$(C_SRCDIRS),$(wildcard $(dir)/*.c)))
CPP_SRCS ?= $(notdir $(foreach dir,$(CXX_SRCDIRS),$(wildcard $(dir)/*.cpp)))
CC_SRCS ?= $(notdir $(foreach dir,$(CXX_SRCDIRS),$(wildcard $(dir)/*.cc)))
CXX_SRCS ?= $(CPP_SRCS) $(CC_SRCS)
ASM_SRCS ?= $(notdir $(foreach dir,$(ASM_SRCDIRS),$(wildcard $(dir)/*.S)))
# If C_SRCS, CXX_SRCS and ASM_SRCS are not defined, use C_OBJS, CXX_OBJS and ASM_OBJS
C_OBJS ?= $(patsubst %.c,%.o,$(C_SRCS))
CPP_OBJS ?= $(patsubst %.cpp,%.o,$(CPP_SRCS))
CC_OBJS ?= $(patsubst %.cc,%.o,$(CC_SRCS))
CXX_OBJS ?= $(CPP_OBJS) $(CC_OBJS) # Note: not used
ASM_OBJS ?= $(patsubst %.S,%.o,$(ASM_SRCS))
$(foreach OBJ,$(C_OBJS),$(eval $(call addTargetCObject,$(OBJ))))
$(foreach OBJ,$(CPP_OBJS),$(eval $(call addTargetCxxObject,$(OBJ),cpp)))
$(foreach OBJ,$(CC_OBJS),$(eval $(call addTargetCxxObject,$(OBJ),cc)))
$(foreach OBJ,$(ASM_OBJS),$(eval $(call addTargetAsmObject,$(OBJ))))
# --------------------------------------------------------------------------------------------
# The following macros are used to create targets in the user Makefile.
# Binaries are built in the cache directory, and then symlinked to the current directory.
# The cache directory is automatically derived from CACHE_ROOT and list of flags and compilers.
# static_library - Create build rules for a static library with caching
# Parameters:
# 1. libName - Library name (becomes output file and phony target)
# 2. objectDeps - Object file dependencies (will be built in cache path)
# The following parameters are all optional:
# 3. extraDeps - Additional dependencies (no cache path prefix)
# 4. postBuildCmds - Extra commands to run after AR
# 5. extraHash - Additional key to compute the unique cache path
# Example:
# $(call static_library,libmath.a,vector.o matrix.o,$(CONFIG_H),strip $@,$(VERSION))
define static_library # libName, objectDeps, extraDeps, postBuildCmds, extraHash
$$(if $$(filter 2,$$(V)),$$(info $$(call $(0),$(1),$(2),$(3),$(4),$(5))))
MCM_ALL_BINS += $(1)
$$(CACHE_ROOT)/%/$(1) : $$(addprefix $$(CACHE_ROOT)/%/,$(2)) $(3)
@echo AR $$@
$$(AR) $$(ARFLAGS) $$@ $$^
$(4)
.PHONY: $(1)
$(1) : ARFLAGS = rcs
$(1) : $$(CACHE_ROOT)/$$(call HASH_FUNC,$(1),$(2) $$(CPPFLAGS) $$(CC) $$(CFLAGS) $$(CXX) $$(CXXFLAGS) $$(AR) $$(ARFLAGS) $(5))/$(1)
$$(LN) -sf $$< $$@
endef # static_library
# c_dynamic_library - Create build rules for a C dynamic/shared library with caching
# Parameters:
# 1. libName - Library name (becomes output file and phony target)
# 2. objectDeps - Object file dependencies (will be built in cache path)
# The following parameters are all optional:
# 3. extraDeps - Additional dependencies (no cache path prefix)
# 4. postLinkCmds - Extra commands to run after linking
# 5. extraHash - Additional key to compute the unique cache path
# Example:
# $(call c_dynamic_library,libmath.so,vector.o matrix.o,$(CONFIG_H),strip $@,$(VERSION))
define c_dynamic_library # libName, objectDeps, extraDeps, postLinkCmds, extraHash
$$(if $$(filter 2,$$(V)),$$(info $$(call $(0),$(1),$(2),$(3),$(4),$(5))))
MCM_ALL_BINS += $(1)
$$(CACHE_ROOT)/%/$(1) : $$(addprefix $$(CACHE_ROOT)/%/,$(2)) $(3)
@echo LD $$@
$$(CC) $$(CPPFLAGS) $$(CFLAGS) $$(LDFLAGS) -shared -o $$@ $$^ $$(LDLIBS)
$(4)
.PHONY: $(1)
$(1) : CFLAGS += -fPIC
$(1) : $$(CACHE_ROOT)/$$(call HASH_FUNC,$(1),$(2) $$(CPPFLAGS) $$(CC) $$(CFLAGS) $$(LDFLAGS) $$(LDLIBS) $(5))/$(1)
$$(LN) -sf $$< $$@
endef # c_dynamic_library
# program_base - Create build rules for an executable program with caching
# Parameters:
# 1. progName - Executable name (becomes output file and phony target)
# 2. objectDeps - Object file dependencies (will be prefixed with cache path)
# Parameters 3 to 5 are optional:
# 3. extraDeps - Additional dependencies (without cache path prefix)
# 4. postLinkCmds - Extra commands to run after linking
# 5. extraHash - Additional data to include in cache path hash
# Parameters 6 & 7 are compulsory:
# 6. compiler - Variable name of compiler to use (CC or CXX)
# 7. compilerFlags - Variable name of compiler flags to use (CFLAGS or CXXFLAGS)
# Example:
# $(call program_base,myapp,main.o utils.o,$(CONFIG_H),strip $@,$(VERSION),CC,CFLAGS)
# $(call program_base,mycppapp,main.o utils.o,$(CONFIG_H),strip $@,$(VERSION),CXX,CXXFLAGS)
define program_base # progName, objectDeps, extraDeps, postLinkCmds, extraHash, compiler, compilerFlags
$$(if $$(filter 2,$$(V)),$$(info $$(call $(0),$(1),$(2),$(3),$(4),$(5),$(6),$(7))))
MCM_ALL_BINS += $(1)
$$(CACHE_ROOT)/%/$(1) : $$(addprefix $$(CACHE_ROOT)/%/,$(2)) $(3)
@echo LD $$@
$$($(6)) $$(CPPFLAGS) $$($(7)) $$^ -o $$@ $$(LDFLAGS) $$(LDLIBS)
$(4)
.PHONY: $(1)
$(1) : $$(CACHE_ROOT)/$$(call HASH_FUNC,$(1),$$($(6)) $$(CPPFLAGS) $$($(7)) $$(LDFLAGS) $$(LDLIBS) $(5))/$(1)
$$(LN) -sf $$< $$@$(EXT)
endef # program_base
# Note: $(EXT) must be set to .exe for Windows
define c_program # progName, objectDeps, extraDeps, postLinkCmds
$$(eval $$(call program_base,$(1),$(2),$(3),$(4),$(1)$(2),CC,CFLAGS))
endef # c_program
define c_program_shared_o # progName, objectDeps, extraDeps, postLinkCmds
$$(eval $$(call program_base,$(1),$(2),$(3),$(4),,CC,CFLAGS))
endef # c_program_shared_o
define cxx_program # progName, objectDeps, extraDeps, postLinkCmds
$$(eval $$(call program_base,$(1),$(2),$(3),$(4),$(1)$(2),CXX,CXXFLAGS))
endef # cxx_program
define cxx_program_shared_o # progName, objectDeps, extraDeps, postLinkCmds
$$(eval $$(call program_base,$(1),$(2),$(3),$(4),,CXX,CXXFLAGS))
endef # cxx_program_shared_o
# --------------------------------------------------------------------------------------------
# Cleaning: delete all objects and binaries created by this script
.PHONY: clean_cache
clean_cache:
$(RM) -rf $(CACHE_ROOT)
$(RM) $(MCM_ALL_BINS)
# automatically attach to standard clean target
.PHONY: clean
clean: clean_cache

42
deps/xxhash/xxhash.c vendored Normal file
View file

@ -0,0 +1,42 @@
/*
* xxHash - Extremely Fast Hash algorithm
* Copyright (C) 2012-2023 Yann Collet
*
* BSD 2-Clause License (https://www.opensource.org/licenses/bsd-license.php)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* You can contact the author at:
* - xxHash homepage: https://www.xxhash.com
* - xxHash source repository: https://github.com/Cyan4973/xxHash
*/
/*
* xxhash.c instantiates functions defined in xxhash.h
*/
#define XXH_STATIC_LINKING_ONLY /* access advanced declarations */
#define XXH_IMPLEMENTATION /* access definitions */
#include "xxhash.h"

7488
deps/xxhash/xxhash.h vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -35,7 +35,7 @@ endif
ifneq ($(OPTIMIZATION),-O0)
OPTIMIZATION+=-fno-omit-frame-pointer
endif
DEPENDENCY_TARGETS=hiredis linenoise lua hdr_histogram fpconv fast_float
DEPENDENCY_TARGETS=hiredis linenoise lua hdr_histogram fpconv fast_float xxhash
NODEPS:=clean distclean
# Default settings
@ -257,7 +257,7 @@ ifdef OPENSSL_PREFIX
endif
# Include paths to dependencies
FINAL_CFLAGS+= -I../deps/hiredis -I../deps/linenoise -I../deps/lua/src -I../deps/hdr_histogram -I../deps/fpconv -I../deps/fast_float
FINAL_CFLAGS+= -I../deps/hiredis -I../deps/linenoise -I../deps/lua/src -I../deps/hdr_histogram -I../deps/fpconv -I../deps/fast_float -I../deps/xxhash
# Determine systemd support and/or build preference (defaulting to auto-detection)
BUILD_WITH_SYSTEMD=no
@ -442,7 +442,7 @@ endif
# redis-server
$(REDIS_SERVER_NAME): $(REDIS_SERVER_OBJ) $(REDIS_VEC_SETS_OBJ)
$(REDIS_LD) -o $@ $^ ../deps/hiredis/libhiredis.a ../deps/lua/src/liblua.a ../deps/hdr_histogram/libhdrhistogram.a ../deps/fpconv/libfpconv.a ../deps/fast_float/libfast_float.a $(FINAL_LIBS)
$(REDIS_LD) -o $@ $^ ../deps/hiredis/libhiredis.a ../deps/lua/src/liblua.a ../deps/hdr_histogram/libhdrhistogram.a ../deps/fpconv/libfpconv.a ../deps/fast_float/libfast_float.a ../deps/xxhash/libxxhash.a $(FINAL_LIBS)
# redis-sentinel
$(REDIS_SENTINEL_NAME): $(REDIS_SERVER_NAME)

View file

@ -10628,6 +10628,63 @@ struct COMMAND_ARG DECRBY_Args[] = {
{MAKE_ARG("decrement",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
};
/********** DELEX ********************/
#ifndef SKIP_CMD_HISTORY_TABLE
/* DELEX history */
#define DELEX_History NULL
#endif
#ifndef SKIP_CMD_TIPS_TABLE
/* DELEX tips */
#define DELEX_Tips NULL
#endif
#ifndef SKIP_CMD_KEY_SPECS_TABLE
/* DELEX key specs */
keySpec DELEX_Keyspecs[1] = {
{NULL,CMD_KEY_RM|CMD_KEY_DELETE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
};
#endif
/* DELEX condition argument table */
struct COMMAND_ARG DELEX_condition_Subargs[] = {
{MAKE_ARG("ifeq-value",ARG_TYPE_STRING,-1,"IFEQ",NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("ifne-value",ARG_TYPE_STRING,-1,"IFNE",NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("ifdeq-digest",ARG_TYPE_INTEGER,-1,"IFDEQ",NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("ifdne-digest",ARG_TYPE_INTEGER,-1,"IFDNE",NULL,NULL,CMD_ARG_NONE,0,NULL)},
};
/* DELEX argument table */
struct COMMAND_ARG DELEX_Args[] = {
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=DELEX_condition_Subargs},
};
/********** DIGEST ********************/
#ifndef SKIP_CMD_HISTORY_TABLE
/* DIGEST history */
#define DIGEST_History NULL
#endif
#ifndef SKIP_CMD_TIPS_TABLE
/* DIGEST tips */
#define DIGEST_Tips NULL
#endif
#ifndef SKIP_CMD_KEY_SPECS_TABLE
/* DIGEST key specs */
keySpec DIGEST_Keyspecs[1] = {
{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
};
#endif
/* DIGEST argument table */
struct COMMAND_ARG DIGEST_Args[] = {
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
};
/********** GET ********************/
#ifndef SKIP_CMD_HISTORY_TABLE
@ -10988,6 +11045,7 @@ commandHistory SET_History[] = {
{"6.0.0","Added the `KEEPTTL` option."},
{"6.2.0","Added the `GET`, `EXAT` and `PXAT` option."},
{"7.0.0","Allowed the `NX` and `GET` options to be used together."},
{"8.4.0","Added 'IFEQ', 'IFNE', 'IFDEQ', 'IFDNE' options."},
};
#endif
@ -11007,6 +11065,10 @@ keySpec SET_Keyspecs[1] = {
struct COMMAND_ARG SET_condition_Subargs[] = {
{MAKE_ARG("nx",ARG_TYPE_PURE_TOKEN,-1,"NX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("xx",ARG_TYPE_PURE_TOKEN,-1,"XX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("ifeq-value",ARG_TYPE_STRING,-1,"IFEQ",NULL,"8.4.0",CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("ifne-value",ARG_TYPE_STRING,-1,"IFNE",NULL,"8.4.0",CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("ifdeq-digest",ARG_TYPE_INTEGER,-1,"IFDEQ",NULL,"8.4.0",CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("ifdne-digest",ARG_TYPE_INTEGER,-1,"IFDNE",NULL,"8.4.0",CMD_ARG_NONE,0,NULL)},
};
/* SET expiration argument table */
@ -11022,7 +11084,7 @@ struct COMMAND_ARG SET_expiration_Subargs[] = {
struct COMMAND_ARG SET_Args[] = {
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("value",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,"2.6.12",CMD_ARG_OPTIONAL,2,NULL),.subargs=SET_condition_Subargs},
{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,"2.6.12",CMD_ARG_OPTIONAL,6,NULL),.subargs=SET_condition_Subargs},
{MAKE_ARG("get",ARG_TYPE_PURE_TOKEN,-1,"GET",NULL,"6.2.0",CMD_ARG_OPTIONAL,0,NULL)},
{MAKE_ARG("expiration",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,5,NULL),.subargs=SET_expiration_Subargs},
};
@ -11496,6 +11558,8 @@ struct COMMAND_STRUCT redisCommandTable[] = {
{MAKE_CMD("append","Appends a string to the value of a key. Creates the key if it doesn't exist.","O(1). The amortized time complexity is O(1) assuming the appended value is small and the already present value is of any size, since the dynamic string library used by Redis will double the free space available on every reallocation.","2.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,APPEND_History,0,APPEND_Tips,0,appendCommand,3,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STRING,APPEND_Keyspecs,1,NULL,2),.args=APPEND_Args},
{MAKE_CMD("decr","Decrements the integer value of a key by one. Uses 0 as initial value if the key doesn't exist.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,DECR_History,0,DECR_Tips,0,decrCommand,2,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STRING,DECR_Keyspecs,1,NULL,1),.args=DECR_Args},
{MAKE_CMD("decrby","Decrements a number from the integer value of a key. Uses 0 as initial value if the key doesn't exist.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,DECRBY_History,0,DECRBY_Tips,0,decrbyCommand,3,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STRING,DECRBY_Keyspecs,1,NULL,2),.args=DECRBY_Args},
{MAKE_CMD("delex","Conditionally removes the specified key based on value or digest comparison.","O(1) for IFEQ/IFNE, O(N) for IFDEQ/IFDNE where N is the length of the string value.","8.4.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,DELEX_History,0,DELEX_Tips,0,delexCommand,-2,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STRING,DELEX_Keyspecs,1,NULL,2),.args=DELEX_Args},
{MAKE_CMD("digest","Returns the XXH3 hash of a string value.","O(N) where N is the length of the string value.","8.4.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,DIGEST_History,0,DIGEST_Tips,0,digestCommand,2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_STRING,DIGEST_Keyspecs,1,NULL,1),.args=DIGEST_Args},
{MAKE_CMD("get","Returns the string value of a key.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GET_History,0,GET_Tips,0,getCommand,2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_STRING,GET_Keyspecs,1,NULL,1),.args=GET_Args},
{MAKE_CMD("getdel","Returns the string value of a key after deleting the key.","O(1)","6.2.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GETDEL_History,0,GETDEL_Tips,0,getdelCommand,2,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STRING,GETDEL_Keyspecs,1,NULL,1),.args=GETDEL_Args},
{MAKE_CMD("getex","Returns the string value of a key after setting its expiration time.","O(1)","6.2.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GETEX_History,0,GETEX_Tips,0,getexCommand,-2,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STRING,GETEX_Keyspecs,1,NULL,2),.args=GETEX_Args},
@ -11509,7 +11573,7 @@ struct COMMAND_STRUCT redisCommandTable[] = {
{MAKE_CMD("mset","Atomically creates or modifies the string values of one or more keys.","O(N) where N is the number of keys to set.","1.0.1",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,MSET_History,0,MSET_Tips,2,msetCommand,-3,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_STRING,MSET_Keyspecs,1,NULL,1),.args=MSET_Args},
{MAKE_CMD("msetnx","Atomically modifies the string values of one or more keys only when all keys don't exist.","O(N) where N is the number of keys to set.","1.0.1",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,MSETNX_History,0,MSETNX_Tips,0,msetnxCommand,-3,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_STRING,MSETNX_Keyspecs,1,NULL,1),.args=MSETNX_Args},
{MAKE_CMD("psetex","Sets both string value and expiration time in milliseconds of a key. The key is created if it doesn't exist.","O(1)","2.6.0",CMD_DOC_DEPRECATED,"`SET` with the `PX` argument","2.6.12","string",COMMAND_GROUP_STRING,PSETEX_History,0,PSETEX_Tips,0,psetexCommand,4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_STRING,PSETEX_Keyspecs,1,NULL,3),.args=PSETEX_Args},
{MAKE_CMD("set","Sets the string value of a key, ignoring its type. The key is created if it doesn't exist.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,SET_History,4,SET_Tips,0,setCommand,-3,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_STRING,SET_Keyspecs,1,setGetKeys,5),.args=SET_Args},
{MAKE_CMD("set","Sets the string value of a key, ignoring its type. The key is created if it doesn't exist.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,SET_History,5,SET_Tips,0,setCommand,-3,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_STRING,SET_Keyspecs,1,setGetKeys,5),.args=SET_Args},
{MAKE_CMD("setex","Sets the string value and expiration time of a key. Creates the key if it doesn't exist.","O(1)","2.0.0",CMD_DOC_DEPRECATED,"`SET` with the `EX` argument","2.6.12","string",COMMAND_GROUP_STRING,SETEX_History,0,SETEX_Tips,0,setexCommand,4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_STRING,SETEX_Keyspecs,1,NULL,3),.args=SETEX_Args},
{MAKE_CMD("setnx","Set the string value of a key only when the key doesn't exist.","O(1)","1.0.0",CMD_DOC_DEPRECATED,"`SET` with the `NX` argument","2.6.12","string",COMMAND_GROUP_STRING,SETNX_History,0,SETNX_Tips,0,setnxCommand,3,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STRING,SETNX_Keyspecs,1,NULL,2),.args=SETNX_Args},
{MAKE_CMD("setrange","Overwrites a part of a string value with another by an offset. Creates the key if it doesn't exist.","O(1), not counting the time taken to copy the new string in place. Usually, this string is very small so the amortized complexity is O(1). Otherwise, complexity is O(M) with M being the length of the value argument.","2.2.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,SETRANGE_History,0,SETRANGE_Tips,0,setrangeCommand,4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_STRING,SETRANGE_Keyspecs,1,NULL,3),.args=SETRANGE_Args},

87
src/commands/delex.json Normal file
View file

@ -0,0 +1,87 @@
{
"DELEX": {
"summary": "Conditionally removes the specified key based on value or digest comparison.",
"complexity": "O(1) for IFEQ/IFNE, O(N) for IFDEQ/IFDNE where N is the length of the string value.",
"group": "string",
"since": "8.4.0",
"arity": -2,
"function": "delexCommand",
"command_flags": [
"WRITE",
"FAST"
],
"acl_categories": [
"STRING"
],
"key_specs": [
{
"flags": [
"RM",
"DELETE"
],
"begin_search": {
"index": {
"pos": 1
}
},
"find_keys": {
"range": {
"lastkey": 0,
"step": 1,
"limit": 0
}
}
}
],
"reply_schema": {
"oneOf": [
{
"description": "The key exists but holds a non-string value",
"const": "WRONGTYPE"
},
{
"description": "The key does not exist or the specified condition was not met.",
"const": 0
},
{
"description": "The key was deleted.",
"const": 1
}
]
},
"arguments": [
{
"name": "key",
"type": "key",
"key_spec_index": 0
},
{
"name": "condition",
"type": "oneof",
"optional": true,
"arguments": [
{
"name": "ifeq-value",
"type": "string",
"token": "IFEQ"
},
{
"name": "ifne-value",
"type": "string",
"token": "IFNE"
},
{
"name": "ifdeq-digest",
"type": "integer",
"token": "IFDEQ"
},
{
"name": "ifdne-digest",
"type": "integer",
"token": "IFDNE"
}
]
}
]
}
}

56
src/commands/digest.json Normal file
View file

@ -0,0 +1,56 @@
{
"DIGEST": {
"summary": "Returns the XXH3 hash of a string value.",
"complexity": "O(N) where N is the length of the string value.",
"group": "string",
"since": "8.4.0",
"arity": 2,
"function": "digestCommand",
"command_flags": [
"READONLY",
"FAST"
],
"acl_categories": [
"STRING"
],
"key_specs": [
{
"flags": [
"RO",
"ACCESS"
],
"begin_search": {
"index": {
"pos": 1
}
},
"find_keys": {
"range": {
"lastkey": 0,
"step": 1,
"limit": 0
}
}
}
],
"reply_schema": {
"oneOf": [
{
"description": "The XXH3 64-bit hash of the string value as a signed integer.",
"type": "string"
},
{
"description": "Key does not exist",
"type": "null"
}
]
},
"arguments": [
{
"name": "key",
"type": "key",
"key_spec_index": 0
}
]
}
}

View file

@ -23,6 +23,10 @@
[
"7.0.0",
"Allowed the `NX` and `GET` options to be used together."
],
[
"8.4.0",
"Added 'IFEQ', 'IFNE', 'IFDEQ', 'IFDNE' options."
]
],
"command_flags": [
@ -58,7 +62,7 @@
"reply_schema": {
"anyOf":[
{
"description": "`GET` not given: Operation was aborted (conflict with one of the `XX`/`NX` options).",
"description": "`GET` not given: Operation was aborted (conflict with one of the `XX`/`NX`/`IFEQ`/`IFDEQ` options or condition not met).",
"type": "null"
},
{
@ -100,6 +104,30 @@
"name": "xx",
"type": "pure-token",
"token": "XX"
},
{
"name": "ifeq-value",
"type": "string",
"token": "IFEQ",
"since": "8.4.0"
},
{
"name": "ifne-value",
"type": "string",
"token": "IFNE",
"since": "8.4.0"
},
{
"name": "ifdeq-digest",
"type": "integer",
"token": "IFDEQ",
"since": "8.4.0"
},
{
"name": "ifdne-digest",
"type": "integer",
"token": "IFDNE",
"since": "8.4.0"
}
]
},

View file

@ -1198,6 +1198,94 @@ void delCommand(client *c) {
delGenericCommand(c,server.lazyfree_lazy_user_del);
}
/* DELEX key [IFEQ match-value|IFNE match-value|IFDEQ match-digest|IFDNE match-digest]
*
* Conditionally removes the specified key. A key is ignored if it does not
* exist.
* If no condition is specified the behavior is the same as DEL command.
* If condition is specified the key must be of STRING type.
*
* IFEQ/IFNE conditions check the match-value against the value of the key
* IFDEQ/IFDNE conditions check the match-digest against the digest of the key's value.*/
void delexCommand(client *c) {
kvobj *o;
int deleted = 0, should_delete = 0;
robj *key = c->argv[1];
o = lookupKeyRead(c->db, key);
if (o == NULL) {
addReplyLongLong(c, 0);
return;
}
/* If there are no conditions specified we just delete the key */
if (c->argc == 2) {
delGenericCommand(c, server.lazyfree_lazy_server_del);
return;
}
/* If any conditions are specified the only supported key type for now is
* string */
if (o->type != OBJ_STRING) {
addReplyError(c, "Key should be of string type if conditions are specified");
return;
}
/* If we have more than two arguments the next two are condition and
* match-value */
if (c->argc != 4) {
addReplyErrorArity(c);
return;
}
char *condition = c->argv[2]->ptr;
if (!strcasecmp("ifeq", condition)) {
robj *valueobj = getDecodedObject(o);
sds match_value = c->argv[3]->ptr;
if (sdscmp(valueobj->ptr, match_value) == 0)
should_delete = 1;
decrRefCount(valueobj);
} else if (!strcasecmp("ifne", condition)) {
robj *valueobj = getDecodedObject(o);
sds match_value = c->argv[3]->ptr;
if (sdscmp(valueobj->ptr, match_value) != 0)
should_delete = 1;
decrRefCount(valueobj);
} else if (!strcasecmp("ifdeq", condition)) {
sds current_digest = stringDigest(o);
if (sdscmp(current_digest, c->argv[3]->ptr) == 0)
should_delete = 1;
sdsfree(current_digest);
} else if (!strcasecmp("ifdne", condition)) {
sds current_digest = stringDigest(o);
if (sdscmp(current_digest, c->argv[3]->ptr) != 0)
should_delete = 1;
sdsfree(current_digest);
} else {
addReplyError(c, "Invalid condition. Use IFEQ, IFNE, IFDEQ, or IFDNE");
return;
}
if (should_delete) {
deleted = server.lazyfree_lazy_server_del ?
dbAsyncDelete(c->db, key) :
dbSyncDelete(c->db, key);
}
if (deleted) {
rewriteClientCommandVector(c, 2, shared.del, key);
signalModifiedKey(c, c->db, key);
notifyKeyspaceEvent(NOTIFY_GENERIC, "del", key, c->db->id);
server.dirty++;
}
addReplyLongLong(c, deleted);
}
void unlinkCommand(client *c) {
delGenericCommand(c,1);
}

View file

@ -3881,6 +3881,9 @@ uint64_t redisBuildId(void);
const char *redisBuildIdRaw(void);
char *redisBuildIdString(void);
/* XXH3 hash of a string as hex string */
sds stringDigest(robj *o);
/* Commands prototypes */
void authCommand(client *c);
void pingCommand(client *c);
@ -3901,6 +3904,7 @@ void getCommand(client *c);
void getexCommand(client *c);
void getdelCommand(client *c);
void delCommand(client *c);
void delexCommand(client *c);
void unlinkCommand(client *c);
void existsCommand(client *c);
void setbitCommand(client *c);
@ -4156,6 +4160,7 @@ void lcsCommand(client *c);
void quitCommand(client *c);
void resetCommand(client *c);
void failoverCommand(client *c);
void digestCommand(client *c);
#if defined(__GNUC__)
void *calloc(size_t count, size_t size) __attribute__ ((deprecated));

View file

@ -8,6 +8,7 @@
*/
#include "server.h"
#include "xxhash.h"
#include <math.h> /* isnan(), isinf() */
/* Forward declarations */
@ -35,11 +36,15 @@ static int checkStringLength(client *c, long long size, long long append) {
* options and variants. This function is called in order to implement the
* following commands: SET, SETEX, PSETEX, SETNX, GETSET.
*
* 'flags' changes the behavior of the command (NX, XX or GET, see below).
* 'flags' changes the behavior of the command (NX, XX, GET, IFEQ, IFNE, IFDEQ
* or IFDNE - see below).
*
* 'expire' represents an expire to set in form of a Redis object as passed
* by the user. It is interpreted according to the specified 'unit'.
*
* 'match_value' is a value to check against if any of IFEQ/IFNE/IFDEQ/IFDNE is
* present.
*
* 'ok_reply' and 'abort_reply' is what the function will reply to the client
* if the operation is performed, or when it is not because of NX or
* XX flags.
@ -57,6 +62,10 @@ static int checkStringLength(client *c, long long size, long long append) {
#define OBJ_EXAT (1<<6) /* Set if timestamp in second is given */
#define OBJ_PXAT (1<<7) /* Set if timestamp in ms is given */
#define OBJ_PERSIST (1<<8) /* Set if we need to remove the ttl */
#define OBJ_SET_IFEQ (1<<9) /* Set if value equals match value */
#define OBJ_SET_IFNE (1<<10) /* Set if value does not equal match value */
#define OBJ_SET_IFDEQ (1<<11) /* Set if current digest equals match digest */
#define OBJ_SET_IFDNE (1<<12) /* Set if current digest does not equal match digest */
/* Forward declaration */
static int getExpireMillisecondsOrReply(client *c, robj *expire, int flags, int unit, long long *milliseconds);
@ -71,7 +80,7 @@ static int getExpireMillisecondsOrReply(client *c, robj *expire, int flags, int
* so that both the database and the caller maintain valid references.
*/
void setGenericCommand(client *c, int flags, robj *key, robj **valref, robj *expire,
int unit, robj *ok_reply, robj *abort_reply)
int unit, robj *match_value, robj *ok_reply, robj *abort_reply)
{
long long milliseconds = 0; /* initialized to avoid any harmless warning */
int found = 0;
@ -89,7 +98,7 @@ void setGenericCommand(client *c, int flags, robj *key, robj **valref, robj *exp
found = (lookupKeyWriteWithLink(c->db,key,&link) != NULL);
if ((flags & OBJ_SET_NX && found) ||
(flags & OBJ_SET_XX && !found))
(flags & (OBJ_SET_XX | OBJ_SET_IFEQ | OBJ_SET_IFDEQ) && !found))
{
if (!(flags & OBJ_SET_GET)) {
addReply(c, abort_reply ? abort_reply : shared.null[c->resp]);
@ -97,6 +106,41 @@ void setGenericCommand(client *c, int flags, robj *key, robj **valref, robj *exp
return;
}
/* Handle conditional set operations - only set if key is found and condition
* is met - otherwise return nil. */
if (found && (flags & (OBJ_SET_IFEQ | OBJ_SET_IFNE | OBJ_SET_IFDEQ | OBJ_SET_IFDNE))) {
kvobj *current = lookupKeyRead(c->db, key);
if (checkType(c, current, OBJ_STRING)) {
return;
}
if (flags & OBJ_SET_IFEQ || flags & OBJ_SET_IFNE) {
robj *current_decoded = getDecodedObject(current);
int condition = (flags & OBJ_SET_IFEQ) ?
sdscmp(current_decoded->ptr, match_value->ptr) == 0 :
sdscmp(current_decoded->ptr, match_value->ptr) != 0;
decrRefCount(current_decoded);
if (!condition) {
if (!(flags & OBJ_SET_GET)) {
addReply(c, abort_reply ? abort_reply : shared.null[c->resp]);
}
return;
}
} else if (flags & OBJ_SET_IFDEQ || flags & OBJ_SET_IFDNE) {
sds current_digest = stringDigest(current);
int condition = flags & OBJ_SET_IFDEQ ?
sdscmp(current_digest, match_value->ptr) == 0 :
sdscmp(current_digest, match_value->ptr) != 0;
sdsfree(current_digest);
if (!condition) {
if (!(flags & OBJ_SET_GET)) {
addReply(c, abort_reply ? abort_reply : shared.null[c->resp]);
}
return;
}
}
}
/* When expire is not NULL, we avoid deleting the TTL so it can be updated later instead of being deleted and then created again. */
setkey_flags |= ((flags & OBJ_KEEPTTL) || expire) ? SETKEY_KEEPTTL : 0;
setkey_flags |= found ? SETKEY_ALREADY_EXIST : SETKEY_DOESNT_EXIST;
@ -194,7 +238,7 @@ static int getExpireMillisecondsOrReply(client *c, robj *expire, int flags, int
* string arguments used in SET and GET command.
*
* Get specific commands - PERSIST/DEL
* Set specific commands - XX/NX/GET
* Set specific commands - XX/NX/GET/IFEQ/IFNE/IFDEQ/IFDNE
* Common commands - EX/EXAT/PX/PXAT/KEEPTTL
*
* Function takes pointers to client, flags, unit, pointer to pointer of expire obj if needed
@ -204,22 +248,36 @@ static int getExpireMillisecondsOrReply(client *c, robj *expire, int flags, int
*
* Input flags are updated upon parsing the arguments. Unit and expire are updated if there are any
* EX/EXAT/PX/PXAT arguments. Unit is updated to millisecond if PX/PXAT is set.
* match_value is updated if any of IFEQ/IFNE/IFDEQ/IFDNE is set.
*/
int parseExtendedStringArgumentsOrReply(client *c, int *flags, int *unit, robj **expire, int command_type) {
int parseExtendedStringArgumentsOrReply(client *c, int *flags, int *unit, robj **expire, robj **match_value, int command_type) {
int j = command_type == COMMAND_GET ? 2 : 3;
/* We can have either none or exactly one of these conditionals as they are
* mutually exclusive. We'll make sure to check if none of the other flags
* are already set if we are going to set one of them. This is done via the
* check:
*
* if (opt == OBJ_SET_XXX && !(*flags & (cond_mut_excl & ~OBJ_SET_XXX)))
*
* A bit ugly - but concise.
*/
int cond_mut_excl = OBJ_SET_NX | OBJ_SET_XX | OBJ_SET_IFEQ | OBJ_SET_IFNE |
OBJ_SET_IFDEQ | OBJ_SET_IFDNE;
for (; j < c->argc; j++) {
char *opt = c->argv[j]->ptr;
robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];
if ((opt[0] == 'n' || opt[0] == 'N') &&
(opt[1] == 'x' || opt[1] == 'X') && opt[2] == '\0' &&
!(*flags & OBJ_SET_XX) && (command_type == COMMAND_SET))
!(*flags & (cond_mut_excl & ~OBJ_SET_NX)) && (command_type == COMMAND_SET))
{
*flags |= OBJ_SET_NX;
} else if ((opt[0] == 'x' || opt[0] == 'X') &&
(opt[1] == 'x' || opt[1] == 'X') && opt[2] == '\0' &&
!(*flags & OBJ_SET_NX) && (command_type == COMMAND_SET))
!(*flags & (cond_mut_excl & ~OBJ_SET_XX)) && (command_type == COMMAND_SET))
{
*flags |= OBJ_SET_XX;
} else if ((opt[0] == 'g' || opt[0] == 'G') &&
@ -281,6 +339,34 @@ int parseExtendedStringArgumentsOrReply(client *c, int *flags, int *unit, robj *
*unit = UNIT_MILLISECONDS;
*expire = next;
j++;
} else if (!strcasecmp(opt, "ifeq") && next &&
!(*flags & (cond_mut_excl & ~OBJ_SET_IFEQ)) &&
(command_type == COMMAND_SET))
{
*flags |= OBJ_SET_IFEQ;
*match_value = next;
j++;
} else if (!strcasecmp(opt, "ifne") && next &&
!(*flags & (cond_mut_excl & ~OBJ_SET_IFNE)) &&
(command_type == COMMAND_SET))
{
*flags |= OBJ_SET_IFNE;
*match_value = next;
j++;
} else if (!strcasecmp(opt, "ifdeq") && next &&
!(*flags & (cond_mut_excl & ~OBJ_SET_IFDEQ)) &&
(command_type == COMMAND_SET))
{
*flags |= OBJ_SET_IFDEQ;
*match_value = next;
j++;
} else if (!strcasecmp(opt, "ifdne") && next &&
!(*flags & (cond_mut_excl & ~OBJ_SET_IFDNE)) &&
(command_type == COMMAND_SET))
{
*flags |= OBJ_SET_IFDNE;
*match_value = next;
j++;
} else {
addReplyErrorObject(c,shared.syntaxerr);
return C_ERR;
@ -290,33 +376,36 @@ int parseExtendedStringArgumentsOrReply(client *c, int *flags, int *unit, robj *
}
/* SET key value [NX] [XX] [KEEPTTL] [GET] [EX <seconds>] [PX <milliseconds>]
* [EXAT <seconds-timestamp>][PXAT <milliseconds-timestamp>] */
* [EXAT <seconds-timestamp>][PXAT <milliseconds-timestamp>]
* [IFEQ <match-value>|IFNE <match-value>|IFDEQ <match-digest>|
* IFDNE <match-digest>]*/
void setCommand(client *c) {
robj *expire = NULL;
robj *match_value = NULL;
int unit = UNIT_SECONDS;
int flags = OBJ_NO_FLAGS;
if (parseExtendedStringArgumentsOrReply(c,&flags,&unit,&expire,COMMAND_SET) != C_OK) {
if (parseExtendedStringArgumentsOrReply(c,&flags,&unit,&expire,&match_value,COMMAND_SET) != C_OK) {
return;
}
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,flags,c->argv[1],&(c->argv[2]),expire,unit,NULL,NULL);
setGenericCommand(c,flags,c->argv[1],&(c->argv[2]),expire,unit,match_value,NULL,NULL);
}
void setnxCommand(client *c) {
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c, OBJ_SET_NX, c->argv[1], &(c->argv[2]), NULL, 0, shared.cone, shared.czero);
setGenericCommand(c, OBJ_SET_NX, c->argv[1], &(c->argv[2]), NULL, 0, NULL, shared.cone, shared.czero);
}
void setexCommand(client *c) {
c->argv[3] = tryObjectEncoding(c->argv[3]);
setGenericCommand(c, OBJ_EX, c->argv[1], &(c->argv[3]), c->argv[2], UNIT_SECONDS, NULL, NULL);
setGenericCommand(c, OBJ_EX, c->argv[1], &(c->argv[3]), c->argv[2], UNIT_SECONDS, NULL, NULL, NULL);
}
void psetexCommand(client *c) {
c->argv[3] = tryObjectEncoding(c->argv[3]);
setGenericCommand(c, OBJ_PX, c->argv[1], &(c->argv[3]), c->argv[2], UNIT_MILLISECONDS, NULL, NULL);
setGenericCommand(c, OBJ_PX, c->argv[1], &(c->argv[3]), c->argv[2], UNIT_MILLISECONDS, NULL, NULL, NULL);
}
int getGenericCommand(client *c) {
@ -362,7 +451,7 @@ void getexCommand(client *c) {
int unit = UNIT_SECONDS;
int flags = OBJ_NO_FLAGS;
if (parseExtendedStringArgumentsOrReply(c,&flags,&unit,&expire,COMMAND_GET) != C_OK) {
if (parseExtendedStringArgumentsOrReply(c,&flags,&unit,&expire,NULL,COMMAND_GET) != C_OK) {
return;
}
@ -962,3 +1051,40 @@ cleanup:
return;
}
/* Return the xxh3 hash of a string object as a hex string stored in an sds.
* The user is responsible for freeing the sds. */
sds stringDigest(robj *o) {
serverAssert(o && o->type == OBJ_STRING);
XXH64_hash_t hash = 0;
if (sdsEncodedObject(o)) {
hash = XXH3_64bits(o->ptr, sdslen(o->ptr));
} else if (o->encoding == OBJ_ENCODING_INT) {
char buf[LONG_STR_SIZE];
size_t len = ll2string(buf,sizeof(buf),(long)o->ptr);
hash = XXH3_64bits(buf, len);
} else {
serverPanic("Wrong obj->encoding stringDigest()");
}
sds hexhash = sdsempty();
hexhash = sdscatprintf(hexhash, "%" PRIx64, hash);
return hexhash;
}
/* DIGEST key
*
* Return digest of the key's value computed via XXH3 hash. The key must be a
* STRING object. */
void digestCommand(client *c) {
kvobj *o;
if ((o = lookupKeyReadOrReply(c, c->argv[1], shared.null[c->resp])) == NULL)
return;
if (checkType(c,o,OBJ_STRING))
return;
addReplyBulkSds(c, stringDigest(o));
}

View file

@ -67,18 +67,18 @@
"ZRANGE k 1 2 rev " "[BYSCORE|BYLEX] [LIMIT offset count] [WITHSCORES]"
"ZRANGE k 1 2 WITHSCORES " "[BYSCORE|BYLEX] [REV] [LIMIT offset count]"
# Optional one-of args with parameters: SET key value [NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]
"SET key value " "[NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value EX" "[NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value EX " "seconds [NX|XX] [GET]"
"SET key value EX 23 " "[NX|XX] [GET]"
"SET key value EXAT" "[NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value EXAT " "unix-time-seconds [NX|XX] [GET]"
"SET key value PX" "[NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value PX " "milliseconds [NX|XX] [GET]"
"SET key value PXAT" "[NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value PXAT " "unix-time-milliseconds [NX|XX] [GET]"
"SET key value KEEPTTL " "[NX|XX] [GET]"
# Optional one-of args with parameters: SET key value [NX|XX|IFEQ ifeq-value|IFNE ifne-value|IFDEQ ifdeq-digest|IFDNE ifdne-digest] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]
"SET key value " "[NX|XX|IFEQ ifeq-value|IFNE ifne-value|IFDEQ ifdeq-digest|IFDNE ifdne-digest] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value EX" "[NX|XX|IFEQ ifeq-value|IFNE ifne-value|IFDEQ ifdeq-digest|IFDNE ifdne-digest] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value EX " "seconds [NX|XX|IFEQ ifeq-value|IFNE ifne-value|IFDEQ ifdeq-digest|IFDNE ifdne-digest] [GET]"
"SET key value EX 23 " "[NX|XX|IFEQ ifeq-value|IFNE ifne-value|IFDEQ ifdeq-digest|IFDNE ifdne-digest] [GET]"
"SET key value EXAT" "[NX|XX|IFEQ ifeq-value|IFNE ifne-value|IFDEQ ifdeq-digest|IFDNE ifdne-digest] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value EXAT " "unix-time-seconds [NX|XX|IFEQ ifeq-value|IFNE ifne-value|IFDEQ ifdeq-digest|IFDNE ifdne-digest] [GET]"
"SET key value PX" "[NX|XX|IFEQ ifeq-value|IFNE ifne-value|IFDEQ ifdeq-digest|IFDNE ifdne-digest] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value PX " "milliseconds [NX|XX|IFEQ ifeq-value|IFNE ifne-value|IFDEQ ifdeq-digest|IFDNE ifdne-digest] [GET]"
"SET key value PXAT" "[NX|XX|IFEQ ifeq-value|IFNE ifne-value|IFDEQ ifdeq-digest|IFDNE ifdne-digest] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value PXAT " "unix-time-milliseconds [NX|XX|IFEQ ifeq-value|IFNE ifne-value|IFDEQ ifdeq-digest|IFDNE ifdne-digest] [GET]"
"SET key value KEEPTTL " "[NX|XX|IFEQ ifeq-value|IFNE ifne-value|IFDEQ ifdeq-digest|IFDNE ifdne-digest] [GET]"
"SET key value XX " "[GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
# If an input word can't be matched, stop hinting.

View file

@ -743,4 +743,673 @@ if {[string match {*jemalloc*} [s mem_allocator]]} {
}
} {} {needs:debug}
}
test {DIGEST basic usage with plain string} {
r set mykey "hello world"
set digest [r digest mykey]
# Ensure reply is hex string
assert {[string is wideinteger -strict "0x$digest"]}
}
test {DIGEST with empty string} {
r set mykey ""
set digest [r digest mykey]
assert {[string is wideinteger -strict "0x$digest"]}
}
test {DIGEST with integer-encoded value} {
r set mykey 12345
assert_encoding int mykey
set digest [r digest mykey]
assert {[string is wideinteger -strict "0x$digest"]}
}
test {DIGEST with negative integer} {
r set mykey -999
assert_encoding int mykey
set digest [r digest mykey]
assert {[string is wideinteger -strict "0x$digest"]}
}
test {DIGEST returns consistent hash for same value} {
r set mykey "test string"
set digest1 [r digest mykey]
set digest2 [r digest mykey]
assert_equal $digest1 $digest2
}
test {DIGEST returns same hash for same content in different keys} {
r set key1 "identical"
r set key2 "identical"
set digest1 [r digest key1]
set digest2 [r digest key2]
assert_equal $digest1 $digest2
}
test {DIGEST returns different hash for different values} {
r set key1 "value1"
r set key2 "value2"
set digest1 [r digest key1]
set digest2 [r digest key2]
assert {$digest1 != $digest2}
}
test {DIGEST with binary data} {
r set mykey "\x00\x01\x02\x03\xff\xfe"
set digest [r digest mykey]
assert {[string is wideinteger -strict "0x$digest"]}
}
test {DIGEST with unicode characters} {
r set mykey "Hello "
set digest [r digest mykey]
assert {[string is wideinteger -strict "0x$digest"]}
}
test {DIGEST with very long string} {
set longstring [string repeat "Lorem ipsum dolor sit amet. " 1000]
r set mykey $longstring
set digest [r digest mykey]
assert {[string is wideinteger -strict "0x$digest"]}
}
test {DIGEST against non-existing key} {
r del nonexistent
assert_equal {} [r digest nonexistent]
}
test {DIGEST against wrong type (list)} {
r del mylist
r lpush mylist "element"
assert_error "*WRONGTYPE*" {r digest mylist}
}
test {DIGEST against wrong type (hash)} {
r del myhash
r hset myhash field value
assert_error "*WRONGTYPE*" {r digest myhash}
}
test {DIGEST against wrong type (set)} {
r del myset
r sadd myset member
assert_error "*WRONGTYPE*" {r digest myset}
}
test {DIGEST against wrong type (zset)} {
r del myzset
r zadd myzset 1 member
assert_error "*WRONGTYPE*" {r digest myzset}
}
test {DIGEST wrong number of arguments} {
assert_error "*wrong number of arguments*" {r digest}
assert_error "*wrong number of arguments*" {r digest key1 key2}
}
test {DIGEST with special characters and whitespace} {
r set mykey " spaces \t\n\r"
set digest [r digest mykey]
assert {[string is wideinteger -strict "0x$digest"]}
}
test {DIGEST consistency across SET operations} {
r set mykey "original"
set digest1 [r digest mykey]
r set mykey "changed"
set digest2 [r digest mykey]
assert {$digest1 != $digest2}
r set mykey "original"
set digest3 [r digest mykey]
assert_equal $digest1 $digest3
}
test {DELEX basic usage without conditions} {
r set mykey "hello"
assert_equal 1 [r delex mykey]
r hset myhash f v
assert_equal 1 [r delex myhash]
r zadd mystr 1 m
assert_equal 1 [r delex mystr]
}
test {DELEX basic usage with IFEQ} {
r set mykey "hello"
assert_equal 1 [r delex mykey IFEQ "hello"]
assert_equal 0 [r exists mykey]
r set mykey "hello"
assert_equal 0 [r delex mykey IFEQ "world"]
assert_equal 1 [r exists mykey]
assert_equal "hello" [r get mykey]
}
test {DELEX basic usage with IFNE} {
r set mykey "hello"
assert_equal 1 [r delex mykey IFNE "world"]
assert_equal 0 [r exists mykey]
r set mykey "hello"
assert_equal 0 [r delex mykey IFNE "hello"]
assert_equal 1 [r exists mykey]
assert_equal "hello" [r get mykey]
}
test {DELEX basic usage with IFDEQ} {
r set mykey "hello"
set digest [r digest mykey]
assert_equal 1 [r delex mykey IFDEQ $digest]
assert_equal 0 [r exists mykey]
r set mykey "hello"
set wrong_digest [format %x [expr [scan [r digest mykey] %x] + 1]]
assert_equal 0 [r delex mykey IFDEQ $wrong_digest]
assert_equal 1 [r exists mykey]
assert_equal "hello" [r get mykey]
}
test {DELEX basic usage with IFDNE} {
r set mykey "hello"
set wrong_digest [format %x [expr [scan [r digest mykey] %x] + 1]]
assert_equal 1 [r delex mykey IFDNE $wrong_digest]
assert_equal 0 [r exists mykey]
r set mykey "hello"
set digest [r digest mykey]
assert_equal 0 [r delex mykey IFDNE $digest]
assert_equal 1 [r exists mykey]
assert_equal "hello" [r get mykey]
}
test {DELEX with non-existing key} {
r del nonexistent
assert_equal 0 [r delex nonexistent IFEQ "hello"]
assert_equal 0 [r delex nonexistent IFNE "hello"]
assert_equal 0 [r delex nonexistent IFDEQ 1234567890]
assert_equal 0 [r delex nonexistent IFDNE 1234567890]
}
test {DELEX with empty string value} {
r set mykey ""
assert_equal 1 [r delex mykey IFEQ ""]
assert_equal 0 [r exists mykey]
r set mykey ""
assert_equal 0 [r delex mykey IFEQ "notempty"]
assert_equal 1 [r exists mykey]
}
test {DELEX with integer-encoded value} {
r set mykey 12345
assert_encoding int mykey
assert_equal 1 [r delex mykey IFEQ "12345"]
assert_equal 0 [r exists mykey]
r set mykey 12345
assert_encoding int mykey
assert_equal 0 [r delex mykey IFEQ "54321"]
assert_equal 1 [r exists mykey]
}
test {DELEX with negative integer} {
r set mykey -999
assert_encoding int mykey
assert_equal 1 [r delex mykey IFEQ "-999"]
assert_equal 0 [r exists mykey]
}
test {DELEX with binary data} {
r set mykey "\x00\x01\x02\x03\xff\xfe"
assert_equal 1 [r delex mykey IFEQ "\x00\x01\x02\x03\xff\xfe"]
assert_equal 0 [r exists mykey]
r set mykey "\x00\x01\x02\x03\xff\xfe"
assert_equal 0 [r delex mykey IFEQ "\x00\x01\x02\x03\xff\xff"]
assert_equal 1 [r exists mykey]
}
test {DELEX with unicode characters} {
r set mykey "Hello "
assert_equal 1 [r delex mykey IFEQ "Hello "]
assert_equal 0 [r exists mykey]
r set mykey "Hello "
assert_equal 0 [r delex mykey IFEQ "Hello World"]
assert_equal 1 [r exists mykey]
}
test {DELEX with very long string} {
set longstring [string repeat "Lorem ipsum dolor sit amet. " 1000]
r set mykey $longstring
assert_equal 1 [r delex mykey IFEQ $longstring]
assert_equal 0 [r exists mykey]
}
test {DELEX against wrong type} {
r del mylist
r lpush mylist "element"
assert_error "*ERR*" {r delex mylist IFEQ "element"}
r del myhash
r hset myhash field value
assert_error "*ERR*" {r delex myhash IFEQ "value"}
r del myset
r sadd myset member
assert_error "*ERR*" {r delex myset IFEQ "member"}
r del myzset
r zadd myzset 1 member
assert_error "*ERR*" {r delex myzset IFEQ "member"}
}
test {DELEX wrong number of arguments} {
r del key1
assert_equal 0 [r delex key1 IFEQ]
r set key1 x
assert_error "*wrong number of arguments*" {r delex key1 IFEQ}
assert_error "*wrong number of arguments*" {r delex key1 IFEQ value1 extra}
}
test {DELEX invalid condition} {
r set mykey "hello"
assert_error "*Invalid condition*" {r delex mykey INVALID "hello"}
assert_error "*Invalid condition*" {r delex mykey IF "hello"}
assert_error "*Invalid condition*" {r delex mykey EQ "hello"}
}
test {DELEX with special characters and whitespace} {
r set mykey " spaces \t\n\r"
assert_equal 1 [r delex mykey IFEQ " spaces \t\n\r"]
assert_equal 0 [r exists mykey]
}
test {DELEX digest consistency with same content} {
r set key1 "identical"
r set key2 "identical"
set digest1 [r digest key1]
set digest2 [r digest key2]
assert_equal $digest1 $digest2
# Both should be deletable with the same digest
assert_equal 1 [r delex key1 IFDEQ $digest2]
assert_equal 1 [r delex key2 IFDEQ $digest1]
}
test {DELEX digest with different content} {
r set key1 "value1"
r set key2 "value2"
set digest1 [r digest key1]
set digest2 [r digest key2]
assert {$digest1 != $digest2}
# Should not be able to delete with wrong digest
assert_equal 0 [r delex key1 IFDEQ $digest2]
assert_equal 0 [r delex key2 IFDEQ $digest1]
# Should be able to delete with correct digest
assert_equal 1 [r delex key1 IFDEQ $digest1]
assert_equal 1 [r delex key2 IFDEQ $digest2]
}
test {DELEX propagate as DEL command to replica} {
set repl [attach_to_replication_stream]
r set foo bar
r delex foo IFEQ bar
assert_replication_stream $repl {
{select *}
{set foo bar}
{del foo}
}
close_replication_stream $repl
} {} {needs:repl}
test {DELEX does not propagate when condition not met} {
set repl [attach_to_replication_stream]
r set foo bar
r delex foo IFEQ baz
r set foo bar2
assert_replication_stream $repl {
{select *}
{set foo bar}
{set foo bar2}
}
close_replication_stream $repl
} {} {needs:repl}
test {DELEX with integer that looks like string} {
# Set as integer
r set key1 123
assert_encoding int key1
assert_equal 1 [r delex key1 IFEQ "123"]
assert_equal 0 [r exists key1]
# Set as string
r set key2 "123"
assert_equal 1 [r delex key2 IFEQ "123"]
assert_equal 0 [r exists key2]
}
test {Extended SET with IFEQ - key exists and matches} {
r set mykey "hello"
assert_equal "OK" [r set mykey "world" IFEQ "hello"]
assert_equal "world" [r get mykey]
}
test {Extended SET with IFEQ - key exists but doesn't match} {
r set mykey "hello"
assert_equal {} [r set mykey "world" IFEQ "different"]
assert_equal "hello" [r get mykey]
}
test {Extended SET with IFEQ - key doesn't exist} {
r del mykey
assert_equal {} [r set mykey "world" IFEQ "hello"]
assert_equal 0 [r exists mykey]
}
test {Extended SET with IFNE - key exists and doesn't match} {
r set mykey "hello"
assert_equal "OK" [r set mykey "world" IFNE "different"]
assert_equal "world" [r get mykey]
}
test {Extended SET with IFNE - key exists and matches} {
r set mykey "hello"
assert_equal {} [r set mykey "world" IFNE "hello"]
assert_equal "hello" [r get mykey]
}
test {Extended SET with IFNE - key doesn't exist} {
r del mykey
assert_equal "OK" [r set mykey "world" IFNE "hello"]
assert_equal "world" [r get mykey]
}
test {Extended SET with IFDEQ - key exists and digest matches} {
r set mykey "hello"
set digest [r digest mykey]
puts $digest
assert_equal "OK" [r set mykey "world" IFDEQ $digest]
assert_equal "world" [r get mykey]
}
test {Extended SET with IFDEQ - key exists but digest doesn't match} {
r set mykey "hello"
set wrong_digest [format %x [expr [scan [r digest mykey] %x] + 1]]
assert_equal {} [r set mykey "world" IFDEQ $wrong_digest]
assert_equal "hello" [r get mykey]
}
test {Extended SET with IFDEQ - key doesn't exist} {
r del mykey
set digest 1234567890
assert_equal {} [r set mykey "world" IFDEQ $digest]
assert_equal 0 [r exists mykey]
}
test {Extended SET with IFDNE - key exists and digest doesn't match} {
r set mykey "hello"
set wrong_digest [format %x [expr [scan [r digest mykey] %x] + 1]]
assert_equal "OK" [r set mykey "world" IFDNE $wrong_digest]
assert_equal "world" [r get mykey]
}
test {Extended SET with IFDNE - key exists and digest matches} {
r set mykey "hello"
set digest [r digest mykey]
assert_equal {} [r set mykey "world" IFDNE $digest]
assert_equal "hello" [r get mykey]
}
test {Extended SET with IFDNE - key doesn't exist} {
r del mykey
set digest 1234567890
assert_equal "OK" [r set mykey "world" IFDNE $digest]
assert_equal "world" [r get mykey]
}
test {Extended SET with IFEQ and GET - key exists and matches} {
r set mykey "hello"
assert_equal "hello" [r set mykey "world" IFEQ "hello" GET]
assert_equal "world" [r get mykey]
}
test {Extended SET with IFEQ and GET - key exists but doesn't match} {
r set mykey "hello"
assert_equal "hello" [r set mykey "world" IFEQ "different" GET]
assert_equal "hello" [r get mykey]
}
test {Extended SET with IFEQ and GET - key doesn't exist} {
r del mykey
assert_equal {} [r set mykey "world" IFEQ "hello" GET]
assert_equal 0 [r exists mykey]
}
test {Extended SET with IFNE and GET - key exists and doesn't match} {
r set mykey "hello"
assert_equal "hello" [r set mykey "world" IFNE "different" GET]
assert_equal "world" [r get mykey]
}
test {Extended SET with IFNE and GET - key exists and matches} {
r set mykey "hello"
assert_equal "hello" [r set mykey "world" IFNE "hello" GET]
assert_equal "hello" [r get mykey]
}
test {Extended SET with IFNE and GET - key doesn't exist} {
r del mykey
assert_equal {} [r set mykey "world" IFNE "hello" GET]
assert_equal "world" [r get mykey]
}
test {Extended SET with IFDEQ and GET - key exists and digest matches} {
r set mykey "hello"
set digest [r digest mykey]
assert_equal "hello" [r set mykey "world" IFDEQ $digest GET]
assert_equal "world" [r get mykey]
}
test {Extended SET with IFDEQ and GET - key exists but digest doesn't match} {
r set mykey "hello"
set wrong_digest [format %x [expr [scan [r digest mykey] %x] + 1]]
assert_equal "hello" [r set mykey "world" IFDEQ $wrong_digest GET]
assert_equal "hello" [r get mykey]
}
test {Extended SET with IFDEQ and GET - key doesn't exist} {
r del mykey
set digest 1234567890
assert_equal {} [r set mykey "world" IFDEQ $digest GET]
assert_equal 0 [r exists mykey]
}
test {Extended SET with IFDNE and GET - key exists and digest doesn't match} {
r set mykey "hello"
set wrong_digest [format %x [expr [scan [r digest mykey] %x] + 1]]
assert_equal "hello" [r set mykey "world" IFDNE $wrong_digest GET]
assert_equal "world" [r get mykey]
}
test {Extended SET with IFDNE and GET - key exists and digest matches} {
r set mykey "hello"
set digest [r digest mykey]
assert_equal "hello" [r set mykey "world" IFDNE $digest GET]
assert_equal "hello" [r get mykey]
}
test {Extended SET with IFDNE and GET - key doesn't exist} {
r del mykey
set digest 1234567890
assert_equal {} [r set mykey "world" IFDNE $digest GET]
assert_equal "world" [r get mykey]
}
test {Extended SET with IFEQ and expiration} {
r set mykey "hello"
assert_equal "OK" [r set mykey "world" IFEQ "hello" EX 10]
assert_equal "world" [r get mykey]
assert_range [r ttl mykey] 5 10
}
test {Extended SET with IFNE and expiration} {
r set mykey "hello"
assert_equal "OK" [r set mykey "world" IFNE "different" EX 10]
assert_equal "world" [r get mykey]
assert_range [r ttl mykey] 5 10
}
test {Extended SET with IFDEQ and expiration} {
r set mykey "hello"
set digest [r digest mykey]
assert_equal "OK" [r set mykey "world" IFDEQ $digest EX 10]
assert_equal "world" [r get mykey]
assert_range [r ttl mykey] 5 10
}
test {Extended SET with IFDNE and expiration} {
r set mykey "hello"
set wrong_digest [format %x [expr [scan [r digest mykey] %x] + 1]]
assert_equal "OK" [r set mykey "world" IFDNE $wrong_digest EX 10]
assert_equal "world" [r get mykey]
assert_range [r ttl mykey] 5 10
}
test {Extended SET with IFEQ against wrong type} {
r del mylist
r lpush mylist "element"
assert_error "*WRONGTYPE*" {r set mylist "value" IFEQ "element"}
}
test {Extended SET with IFNE against wrong type} {
r del myhash
r hset myhash field value
assert_error "*WRONGTYPE*" {r set myhash "value" IFNE "value"}
}
test {Extended SET with IFDEQ against wrong type} {
r del myset
r sadd myset member
assert_error "*WRONGTYPE*" {r set myset "value" IFDEQ 1234567890}
}
test {Extended SET with IFDNE against wrong type} {
r del myzset
r zadd myzset 1 member
assert_error "*WRONGTYPE*" {r set myzset "value" IFDNE 1234567890}
}
test {Extended SET with integer-encoded value and IFEQ} {
r set mykey 12345
assert_encoding int mykey
assert_equal "OK" [r set mykey "world" IFEQ "12345"]
assert_equal "world" [r get mykey]
}
test {Extended SET with integer-encoded value and IFNE} {
r set mykey 12345
assert_encoding int mykey
assert_equal "OK" [r set mykey "world" IFNE "54321"]
assert_equal "world" [r get mykey]
}
test {Extended SET with binary data and IFEQ} {
r set mykey "\x00\x01\x02\x03\xff\xfe"
assert_equal "OK" [r set mykey "world" IFEQ "\x00\x01\x02\x03\xff\xfe"]
assert_equal "world" [r get mykey]
}
test {Extended SET with unicode characters and IFEQ} {
r set mykey "Hello "
assert_equal "OK" [r set mykey "world" IFEQ "Hello "]
assert_equal "world" [r get mykey]
}
test {Extended SET with empty string and IFEQ} {
r set mykey ""
assert_equal "OK" [r set mykey "world" IFEQ ""]
assert_equal "world" [r get mykey]
}
test {Extended SET with empty string and IFNE} {
r set mykey ""
assert_equal {} [r set mykey "world" IFNE ""]
assert_equal "" [r get mykey]
}
test {Extended SET case insensitive conditions} {
r set mykey "hello"
assert_equal "OK" [r set mykey "world" ifeq "hello"]
assert_equal "world" [r get mykey]
r set mykey "hello"
assert_equal "OK" [r set mykey "world" IfEq "hello"]
assert_equal "world" [r get mykey]
r set mykey "hello"
assert_equal "OK" [r set mykey "world" IFEQ "hello"]
assert_equal "world" [r get mykey]
}
test {Extended SET with special characters and IFEQ} {
r set mykey " spaces \t\n\r"
assert_equal "OK" [r set mykey "world" IFEQ " spaces \t\n\r"]
assert_equal "world" [r get mykey]
}
test {Extended SET digest consistency with same content} {
r set key1 "identical"
r set key2 "identical"
set digest1 [r digest key1]
set digest2 [r digest key2]
assert_equal $digest1 $digest2
# Both should be settable with the same digest
assert_equal "OK" [r set key1 "new1" IFDEQ $digest1]
assert_equal "OK" [r set key2 "new2" IFDEQ $digest2]
assert_equal "new1" [r get key1]
assert_equal "new2" [r get key2]
}
test {Extended SET digest with different content} {
r set key1 "value1"
r set key2 "value2"
set digest1 [r digest key1]
set digest2 [r digest key2]
assert {$digest1 != $digest2}
# Should not be able to set with wrong digest
assert_equal {} [r set key1 "new1" IFDEQ $digest2]
assert_equal {} [r set key2 "new2" IFDEQ $digest1]
assert_equal "value1" [r get key1]
assert_equal "value2" [r get key2]
# Should be able to set with correct digest
assert_equal "OK" [r set key1 "new1" IFDEQ $digest1]
assert_equal "OK" [r set key2 "new2" IFDEQ $digest2]
assert_equal "new1" [r get key1]
assert_equal "new2" [r get key2]
}
test {Extended SET with very long string and IFEQ} {
set longstring [string repeat "Lorem ipsum dolor sit amet. " 1000]
r set mykey $longstring
assert_equal "OK" [r set mykey "world" IFEQ $longstring]
assert_equal "world" [r get mykey]
}
test {Extended SET with negative digest} {
r set mykey "test"
set digest [r digest mykey]
set wrong_digest [format %x [expr [scan [r digest mykey] %x] + 1]]
assert_equal "OK" [r set mykey "world" IFDNE $wrong_digest]
assert_equal "world" [r get mykey]
}
}