Compare commits
89 Commits
add_alter_
...
next-3.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd293768e7 | ||
|
|
22dfa48585 | ||
|
|
2f3d7f1408 | ||
|
|
76a08df939 | ||
|
|
6aa129d3b0 | ||
|
|
b4f781e4eb | ||
|
|
27594ca50e | ||
|
|
0f2f0d65d7 | ||
|
|
31c2f8a3ae | ||
|
|
ec12331f11 | ||
|
|
ccc463b5e5 | ||
|
|
4a9676f6b7 | ||
|
|
aaf4989c31 | ||
|
|
b29f954f20 | ||
|
|
5546d5df7b | ||
|
|
541c29677f | ||
|
|
06f18108c0 | ||
|
|
90002ca3d2 | ||
|
|
da23902311 | ||
|
|
2b0dc21f97 | ||
|
|
b544691493 | ||
|
|
d420b06844 | ||
|
|
b3a2cb2f68 | ||
|
|
c8c057f5f8 | ||
|
|
038bfc925c | ||
|
|
13a4e7db83 | ||
|
|
727d6cf8f3 | ||
|
|
6d6d7b4abe | ||
|
|
28f974b810 | ||
|
|
5fdadcaf3b | ||
|
|
a960394f27 | ||
|
|
3216a1a70a | ||
|
|
5a7fd41618 | ||
|
|
dd24ba7a62 | ||
|
|
204f6dd393 | ||
|
|
b1278adc15 | ||
|
|
ee9677ef71 | ||
|
|
2060e361cf | ||
|
|
6f939ffe19 | ||
|
|
69105bde8a | ||
|
|
e09e9a5929 | ||
|
|
2308bdbccb | ||
|
|
a2d39c9a2e | ||
|
|
5fe2ce3bbe | ||
|
|
aafa34bbad | ||
|
|
7ae2cdf46c | ||
|
|
863f88c067 | ||
|
|
90b4e9e595 | ||
|
|
434ad4548f | ||
|
|
cbbb15af5c | ||
|
|
3231580c05 | ||
|
|
62364d9dcd | ||
|
|
3bed8063f6 | ||
|
|
413fcab833 | ||
|
|
9f3c3036bf | ||
|
|
ff2e108a6d | ||
|
|
ade788ffe8 | ||
|
|
1f8bb754d9 | ||
|
|
7b2eb09225 | ||
|
|
d2293f9fd5 | ||
|
|
25b31f6c23 | ||
|
|
742a1ce7d6 | ||
|
|
4ca9d23b83 | ||
|
|
9e97f3a9b3 | ||
|
|
183418f228 | ||
|
|
756574d094 | ||
|
|
a348418918 | ||
|
|
06c0bd0681 | ||
|
|
223c300435 | ||
|
|
ac8bef6781 | ||
|
|
68691907af | ||
|
|
f59d2fcbf1 | ||
|
|
bdc542143e | ||
|
|
061a02237c | ||
|
|
35b6505517 | ||
|
|
866c04dd64 | ||
|
|
dc588e6e7b | ||
|
|
f842154453 | ||
|
|
b38193f71d | ||
|
|
f47ba6dc06 | ||
|
|
0d0c1d4318 | ||
|
|
9225b17b99 | ||
|
|
00b3f28199 | ||
|
|
1bbe619689 | ||
|
|
c36f71c783 | ||
|
|
f5471d268b | ||
|
|
fd5c65d9dc | ||
|
|
3aa406bf00 | ||
|
|
c0253d9221 |
@@ -1,4 +1,3 @@
|
||||
.git
|
||||
build
|
||||
seastar/build
|
||||
testlog
|
||||
|
||||
87
.github/CODEOWNERS
vendored
87
.github/CODEOWNERS
vendored
@@ -1,87 +0,0 @@
|
||||
# AUTH
|
||||
auth/* @elcallio @vladzcloudius
|
||||
|
||||
# CACHE
|
||||
row_cache* @tgrabiec @haaawk
|
||||
*mutation* @tgrabiec @haaawk
|
||||
tests/mvcc* @tgrabiec @haaawk
|
||||
|
||||
# CDC
|
||||
cdc/* @haaawk @kbr- @elcallio @piodul @jul-stas
|
||||
test/cql/cdc_* @haaawk @kbr- @elcallio @piodul @jul-stas
|
||||
test/boost/cdc_* @haaawk @kbr- @elcallio @piodul @jul-stas
|
||||
|
||||
# COMMITLOG / BATCHLOG
|
||||
db/commitlog/* @elcallio
|
||||
db/batch* @elcallio
|
||||
|
||||
# COORDINATOR
|
||||
service/storage_proxy* @gleb-cloudius
|
||||
|
||||
# COMPACTION
|
||||
sstables/compaction* @raphaelsc @nyh
|
||||
|
||||
# CQL TRANSPORT LAYER
|
||||
transport/* @penberg
|
||||
|
||||
# CQL QUERY LANGUAGE
|
||||
cql3/* @tgrabiec @penberg @psarna
|
||||
|
||||
# COUNTERS
|
||||
counters* @haaawk @jul-stas
|
||||
tests/counter_test* @haaawk @jul-stas
|
||||
|
||||
# GOSSIP
|
||||
gms/* @tgrabiec @asias
|
||||
|
||||
# DOCKER
|
||||
dist/docker/* @penberg
|
||||
|
||||
# LSA
|
||||
utils/logalloc* @tgrabiec
|
||||
|
||||
# MATERIALIZED VIEWS
|
||||
db/view/* @nyh @psarna
|
||||
cql3/statements/*view* @nyh @psarna
|
||||
test/boost/view_* @nyh @psarna
|
||||
|
||||
# PACKAGING
|
||||
dist/* @syuu1228
|
||||
|
||||
# REPAIR
|
||||
repair/* @tgrabiec @asias @nyh
|
||||
|
||||
# SCHEMA MANAGEMENT
|
||||
db/schema_tables* @tgrabiec @nyh
|
||||
db/legacy_schema_migrator* @tgrabiec @nyh
|
||||
service/migration* @tgrabiec @nyh
|
||||
schema* @tgrabiec @nyh
|
||||
|
||||
# SECONDARY INDEXES
|
||||
db/index/* @nyh @penberg @psarna
|
||||
cql3/statements/*index* @nyh @penberg @psarna
|
||||
test/boost/*index* @nyh @penberg @psarna
|
||||
|
||||
# SSTABLES
|
||||
sstables/* @tgrabiec @raphaelsc @nyh
|
||||
|
||||
# STREAMING
|
||||
streaming/* @tgrabiec @asias
|
||||
service/storage_service.* @tgrabiec @asias
|
||||
|
||||
# ALTERNATOR
|
||||
alternator/* @nyh @psarna
|
||||
test/alternator/* @nyh @psarna
|
||||
|
||||
# HINTED HANDOFF
|
||||
db/hints/* @haaawk @piodul @vladzcloudius
|
||||
|
||||
# REDIS
|
||||
redis/* @nyh @syuu1228
|
||||
redis-test/* @nyh @syuu1228
|
||||
|
||||
# READERS
|
||||
reader_* @denesb
|
||||
querier* @denesb
|
||||
test/boost/mutation_reader_test.cc @denesb
|
||||
test/boost/querier_cache_test.cc @denesb
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -22,6 +22,5 @@ resources
|
||||
.pytest_cache
|
||||
/expressions.tokens
|
||||
tags
|
||||
testlog
|
||||
testlog/*
|
||||
test/*/*.reject
|
||||
.vscode
|
||||
|
||||
20
.gitmodules
vendored
20
.gitmodules
vendored
@@ -1,23 +1,17 @@
|
||||
[submodule "seastar"]
|
||||
path = seastar
|
||||
url = ../seastar
|
||||
url = ../scylla-seastar
|
||||
ignore = dirty
|
||||
[submodule "swagger-ui"]
|
||||
path = swagger-ui
|
||||
url = ../scylla-swagger-ui
|
||||
ignore = dirty
|
||||
[submodule "xxHash"]
|
||||
path = xxHash
|
||||
url = ../xxHash
|
||||
[submodule "libdeflate"]
|
||||
path = libdeflate
|
||||
url = ../libdeflate
|
||||
[submodule "abseil"]
|
||||
path = abseil
|
||||
url = ../abseil-cpp
|
||||
[submodule "scylla-jmx"]
|
||||
path = tools/jmx
|
||||
url = ../scylla-jmx
|
||||
[submodule "scylla-tools"]
|
||||
path = tools/java
|
||||
url = ../scylla-tools-java
|
||||
[submodule "scylla-python3"]
|
||||
path = tools/python3
|
||||
url = ../scylla-python3
|
||||
[submodule "zstd"]
|
||||
path = zstd
|
||||
url = ../zstd
|
||||
|
||||
851
CMakeLists.txt
851
CMakeLists.txt
@@ -1,755 +1,142 @@
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
##
|
||||
## For best results, first compile the project using the Ninja build-system.
|
||||
##
|
||||
|
||||
cmake_minimum_required(VERSION 3.7)
|
||||
project(scylla)
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
message(STATUS "Setting build type to 'Release' as none was specified.")
|
||||
set(CMAKE_BUILD_TYPE "Release" CACHE
|
||||
STRING "Choose the type of build." FORCE)
|
||||
# Set the possible values of build type for cmake-gui
|
||||
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||
"Debug" "Release" "Dev" "Sanitize")
|
||||
if (NOT DEFINED FOR_IDE AND NOT DEFINED ENV{FOR_IDE} AND NOT DEFINED ENV{CLION_IDE})
|
||||
message(FATAL_ERROR "This CMakeLists.txt file is only valid for use in IDEs, please define FOR_IDE to acknowledge this.")
|
||||
endif()
|
||||
|
||||
if(CMAKE_BUILD_TYPE)
|
||||
string(TOLOWER "${CMAKE_BUILD_TYPE}" BUILD_TYPE)
|
||||
else()
|
||||
set(BUILD_TYPE "release")
|
||||
endif()
|
||||
# Default value. A more accurate list is populated through `pkg-config` below if `seastar.pc` is available.
|
||||
set(SEASTAR_INCLUDE_DIRS "seastar")
|
||||
|
||||
function(default_target_arch arch)
|
||||
set(x86_instruction_sets i386 i686 x86_64)
|
||||
if(CMAKE_SYSTEM_PROCESSOR IN_LIST x86_instruction_sets)
|
||||
set(${arch} "westmere" PARENT_SCOPE)
|
||||
elseif(CMAKE_SYSTEM_PROCESSOR EQUAL "aarch64")
|
||||
set(${arch} "armv8-a+crc+crypto" PARENT_SCOPE)
|
||||
# These paths are always available, since they're included in the repository. Additional DPDK headers are placed while
|
||||
# Seastar is built, and are captured in `SEASTAR_INCLUDE_DIRS` through parsing the Seastar pkg-config file (below).
|
||||
set(SEASTAR_DPDK_INCLUDE_DIRS
|
||||
seastar/dpdk/lib/librte_eal/common/include
|
||||
seastar/dpdk/lib/librte_eal/common/include/generic
|
||||
seastar/dpdk/lib/librte_eal/common/include/x86
|
||||
seastar/dpdk/lib/librte_ether)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
|
||||
set(ENV{PKG_CONFIG_PATH} "${CMAKE_SOURCE_DIR}/seastar/build/release:$ENV{PKG_CONFIG_PATH}")
|
||||
pkg_check_modules(SEASTAR seastar)
|
||||
|
||||
find_package(Boost COMPONENTS filesystem program_options system thread)
|
||||
|
||||
##
|
||||
## Populate the names of all source and header files in the indicated paths in a designated variable.
|
||||
##
|
||||
## When RECURSIVE is specified, directories are traversed recursively.
|
||||
##
|
||||
## Use: scan_scylla_source_directories(VAR my_result_var [RECURSIVE] PATHS [path1 path2 ...])
|
||||
##
|
||||
function (scan_scylla_source_directories)
|
||||
set(options RECURSIVE)
|
||||
set(oneValueArgs VAR)
|
||||
set(multiValueArgs PATHS)
|
||||
cmake_parse_arguments(args "${options}" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}")
|
||||
|
||||
set(globs "")
|
||||
|
||||
foreach (dir ${args_PATHS})
|
||||
list(APPEND globs "${dir}/*.cc" "${dir}/*.hh")
|
||||
endforeach()
|
||||
|
||||
if (args_RECURSIVE)
|
||||
set(glob_kind GLOB_RECURSE)
|
||||
else()
|
||||
set(${arch} "" PARENT_SCOPE)
|
||||
set(glob_kind GLOB)
|
||||
endif()
|
||||
endfunction()
|
||||
default_target_arch(target_arch)
|
||||
if(target_arch)
|
||||
set(target_arch_flag "-march=${target_arch}")
|
||||
endif()
|
||||
|
||||
# Configure Seastar compile options to align with Scylla
|
||||
set(Seastar_CXX_FLAGS -fcoroutines ${target_arch_flag} CACHE INTERNAL "" FORCE)
|
||||
set(Seastar_CXX_DIALECT gnu++20 CACHE INTERNAL "" FORCE)
|
||||
file(${glob_kind} var
|
||||
${globs})
|
||||
|
||||
add_subdirectory(seastar)
|
||||
add_subdirectory(abseil)
|
||||
# Exclude absl::strerror from the default "all" target since it's not
|
||||
# used in Scylla build and, moreover, makes use of deprecated glibc APIs,
|
||||
# such as sys_nerr, which are not exposed from "stdio.h" since glibc 2.32,
|
||||
# which happens to be the case for recent Fedora distribution versions.
|
||||
#
|
||||
# Need to use the internal "absl_strerror" target name instead of namespaced
|
||||
# variant because `set_target_properties` does not understand the latter form,
|
||||
# unfortunately.
|
||||
set_target_properties(absl_strerror PROPERTIES EXCLUDE_FROM_ALL TRUE)
|
||||
|
||||
# System libraries dependencies
|
||||
find_package(Boost COMPONENTS filesystem program_options system thread regex REQUIRED)
|
||||
find_package(Lua REQUIRED)
|
||||
find_package(ZLIB REQUIRED)
|
||||
find_package(ICU COMPONENTS uc REQUIRED)
|
||||
|
||||
set(scylla_build_dir "${CMAKE_BINARY_DIR}/build/${BUILD_TYPE}")
|
||||
set(scylla_gen_build_dir "${scylla_build_dir}/gen")
|
||||
file(MAKE_DIRECTORY "${scylla_build_dir}" "${scylla_gen_build_dir}")
|
||||
|
||||
# Place libraries, executables and archives in ${buildroot}/build/${mode}/
|
||||
foreach(mode RUNTIME LIBRARY ARCHIVE)
|
||||
set(CMAKE_${mode}_OUTPUT_DIRECTORY "${scylla_build_dir}")
|
||||
endforeach()
|
||||
|
||||
# Generate C++ source files from thrift definitions
|
||||
function(scylla_generate_thrift)
|
||||
set(one_value_args TARGET VAR IN_FILE OUT_DIR SERVICE)
|
||||
cmake_parse_arguments(args "" "${one_value_args}" "" ${ARGN})
|
||||
|
||||
get_filename_component(in_file_name ${args_IN_FILE} NAME_WE)
|
||||
|
||||
set(aux_out_file_name ${args_OUT_DIR}/${in_file_name})
|
||||
set(outputs
|
||||
${aux_out_file_name}_types.cpp
|
||||
${aux_out_file_name}_types.h
|
||||
${aux_out_file_name}_constants.cpp
|
||||
${aux_out_file_name}_constants.h
|
||||
${args_OUT_DIR}/${args_SERVICE}.cpp
|
||||
${args_OUT_DIR}/${args_SERVICE}.h)
|
||||
|
||||
add_custom_command(
|
||||
DEPENDS
|
||||
${args_IN_FILE}
|
||||
thrift
|
||||
OUTPUT ${outputs}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${args_OUT_DIR}
|
||||
COMMAND thrift -gen cpp:cob_style,no_skeleton -out "${args_OUT_DIR}" "${args_IN_FILE}")
|
||||
|
||||
add_custom_target(${args_TARGET}
|
||||
DEPENDS ${outputs})
|
||||
|
||||
set(${args_VAR} ${outputs} PARENT_SCOPE)
|
||||
set(${args_VAR} ${var} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
scylla_generate_thrift(
|
||||
TARGET scylla_thrift_gen_cassandra
|
||||
VAR scylla_thrift_gen_cassandra_files
|
||||
IN_FILE interface/cassandra.thrift
|
||||
OUT_DIR ${scylla_gen_build_dir}
|
||||
SERVICE Cassandra)
|
||||
## Although Seastar is an external project, it is common enough to explore the sources while doing
|
||||
## Scylla development that we'll treat the Seastar sources as part of this project for easier navigation.
|
||||
scan_scylla_source_directories(
|
||||
VAR SEASTAR_SOURCE_FILES
|
||||
RECURSIVE
|
||||
|
||||
# Parse antlr3 grammar files and generate C++ sources
|
||||
function(scylla_generate_antlr3)
|
||||
set(one_value_args TARGET VAR IN_FILE OUT_DIR)
|
||||
cmake_parse_arguments(args "" "${one_value_args}" "" ${ARGN})
|
||||
PATHS
|
||||
seastar/core
|
||||
seastar/http
|
||||
seastar/json
|
||||
seastar/net
|
||||
seastar/rpc
|
||||
seastar/tests
|
||||
seastar/util)
|
||||
|
||||
get_filename_component(in_file_pure_name ${args_IN_FILE} NAME)
|
||||
get_filename_component(stem ${in_file_pure_name} NAME_WE)
|
||||
scan_scylla_source_directories(
|
||||
VAR SCYLLA_ROOT_SOURCE_FILES
|
||||
PATHS .)
|
||||
|
||||
set(outputs
|
||||
"${args_OUT_DIR}/${stem}Lexer.hpp"
|
||||
"${args_OUT_DIR}/${stem}Lexer.cpp"
|
||||
"${args_OUT_DIR}/${stem}Parser.hpp"
|
||||
"${args_OUT_DIR}/${stem}Parser.cpp")
|
||||
scan_scylla_source_directories(
|
||||
VAR SCYLLA_SUB_SOURCE_FILES
|
||||
RECURSIVE
|
||||
|
||||
add_custom_command(
|
||||
DEPENDS
|
||||
${args_IN_FILE}
|
||||
OUTPUT ${outputs}
|
||||
# Remove #ifdef'ed code from the grammar source code
|
||||
COMMAND sed -e "/^#if 0/,/^#endif/d" "${args_IN_FILE}" > "${args_OUT_DIR}/${in_file_pure_name}"
|
||||
COMMAND antlr3 "${args_OUT_DIR}/${in_file_pure_name}"
|
||||
# We replace many local `ExceptionBaseType* ex` variables with a single function-scope one.
|
||||
# Because we add such a variable to every function, and because `ExceptionBaseType` is not a global
|
||||
# name, we also add a global typedef to avoid compilation errors.
|
||||
COMMAND sed -i -e "/^.*On :.*$/d" "${args_OUT_DIR}/${stem}Lexer.hpp"
|
||||
COMMAND sed -i -e "/^.*On :.*$/d" "${args_OUT_DIR}/${stem}Lexer.cpp"
|
||||
COMMAND sed -i -e "/^.*On :.*$/d" "${args_OUT_DIR}/${stem}Parser.hpp"
|
||||
COMMAND sed -i
|
||||
-e "s/^\\( *\\)\\(ImplTraits::CommonTokenType\\* [a-zA-Z0-9_]* = NULL;\\)$/\\1const \\2/"
|
||||
-e "/^.*On :.*$/d"
|
||||
-e "1i using ExceptionBaseType = int;"
|
||||
-e "s/^{/{ ExceptionBaseType\\* ex = nullptr;/; s/ExceptionBaseType\\* ex = new/ex = new/; s/exceptions::syntax_exception e/exceptions::syntax_exception\\& e/"
|
||||
"${args_OUT_DIR}/${stem}Parser.cpp"
|
||||
VERBATIM)
|
||||
PATHS
|
||||
api
|
||||
auth
|
||||
cql3
|
||||
db
|
||||
dht
|
||||
exceptions
|
||||
gms
|
||||
index
|
||||
io
|
||||
locator
|
||||
message
|
||||
repair
|
||||
service
|
||||
sstables
|
||||
streaming
|
||||
test
|
||||
thrift
|
||||
tracing
|
||||
transport
|
||||
utils)
|
||||
|
||||
add_custom_target(${args_TARGET}
|
||||
DEPENDS ${outputs})
|
||||
scan_scylla_source_directories(
|
||||
VAR SCYLLA_GEN_SOURCE_FILES
|
||||
RECURSIVE
|
||||
PATHS build/release/gen)
|
||||
|
||||
set(${args_VAR} ${outputs} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
set(antlr3_grammar_files
|
||||
cql3/Cql.g
|
||||
alternator/expressions.g)
|
||||
|
||||
set(antlr3_gen_files)
|
||||
|
||||
foreach(f ${antlr3_grammar_files})
|
||||
get_filename_component(grammar_file_name "${f}" NAME_WE)
|
||||
get_filename_component(f_dir "${f}" DIRECTORY)
|
||||
scylla_generate_antlr3(
|
||||
TARGET scylla_antlr3_gen_${grammar_file_name}
|
||||
VAR scylla_antlr3_gen_${grammar_file_name}_files
|
||||
IN_FILE ${f}
|
||||
OUT_DIR ${scylla_gen_build_dir}/${f_dir})
|
||||
list(APPEND antlr3_gen_files "${scylla_antlr3_gen_${grammar_file_name}_files}")
|
||||
endforeach()
|
||||
|
||||
# Generate C++ sources from ragel grammar files
|
||||
seastar_generate_ragel(
|
||||
TARGET scylla_ragel_gen_protocol_parser
|
||||
VAR scylla_ragel_gen_protocol_parser_file
|
||||
IN_FILE redis/protocol_parser.rl
|
||||
OUT_FILE ${scylla_gen_build_dir}/redis/protocol_parser.hh)
|
||||
|
||||
# Generate C++ sources from Swagger definitions
|
||||
set(swagger_files
|
||||
api/api-doc/cache_service.json
|
||||
api/api-doc/collectd.json
|
||||
api/api-doc/column_family.json
|
||||
api/api-doc/commitlog.json
|
||||
api/api-doc/compaction_manager.json
|
||||
api/api-doc/config.json
|
||||
api/api-doc/endpoint_snitch_info.json
|
||||
api/api-doc/error_injection.json
|
||||
api/api-doc/failure_detector.json
|
||||
api/api-doc/gossiper.json
|
||||
api/api-doc/hinted_handoff.json
|
||||
api/api-doc/lsa.json
|
||||
api/api-doc/messaging_service.json
|
||||
api/api-doc/storage_proxy.json
|
||||
api/api-doc/storage_service.json
|
||||
api/api-doc/stream_manager.json
|
||||
api/api-doc/system.json
|
||||
api/api-doc/utils.json)
|
||||
|
||||
set(swagger_gen_files)
|
||||
|
||||
foreach(f ${swagger_files})
|
||||
get_filename_component(fname "${f}" NAME_WE)
|
||||
get_filename_component(dir "${f}" DIRECTORY)
|
||||
seastar_generate_swagger(
|
||||
TARGET scylla_swagger_gen_${fname}
|
||||
VAR scylla_swagger_gen_${fname}_files
|
||||
IN_FILE "${f}"
|
||||
OUT_DIR "${scylla_gen_build_dir}/${dir}")
|
||||
list(APPEND swagger_gen_files "${scylla_swagger_gen_${fname}_files}")
|
||||
endforeach()
|
||||
|
||||
# Create C++ bindings for IDL serializers
|
||||
function(scylla_generate_idl_serializer)
|
||||
set(one_value_args TARGET VAR IN_FILE OUT_FILE)
|
||||
cmake_parse_arguments(args "" "${one_value_args}" "" ${ARGN})
|
||||
get_filename_component(out_dir ${args_OUT_FILE} DIRECTORY)
|
||||
set(idl_compiler "${CMAKE_SOURCE_DIR}/idl-compiler.py")
|
||||
|
||||
find_package(Python3 COMPONENTS Interpreter)
|
||||
|
||||
add_custom_command(
|
||||
DEPENDS
|
||||
${args_IN_FILE}
|
||||
${idl_compiler}
|
||||
OUTPUT ${args_OUT_FILE}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${out_dir}
|
||||
COMMAND Python3::Interpreter ${idl_compiler} --ns ser -f ${args_IN_FILE} -o ${args_OUT_FILE})
|
||||
|
||||
add_custom_target(${args_TARGET}
|
||||
DEPENDS ${args_OUT_FILE})
|
||||
|
||||
set(${args_VAR} ${args_OUT_FILE} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
set(idl_serializers
|
||||
idl/cache_temperature.idl.hh
|
||||
idl/commitlog.idl.hh
|
||||
idl/consistency_level.idl.hh
|
||||
idl/frozen_mutation.idl.hh
|
||||
idl/frozen_schema.idl.hh
|
||||
idl/gossip_digest.idl.hh
|
||||
idl/idl_test.idl.hh
|
||||
idl/keys.idl.hh
|
||||
idl/messaging_service.idl.hh
|
||||
idl/mutation.idl.hh
|
||||
idl/paging_state.idl.hh
|
||||
idl/partition_checksum.idl.hh
|
||||
idl/paxos.idl.hh
|
||||
idl/query.idl.hh
|
||||
idl/range.idl.hh
|
||||
idl/read_command.idl.hh
|
||||
idl/reconcilable_result.idl.hh
|
||||
idl/replay_position.idl.hh
|
||||
idl/result.idl.hh
|
||||
idl/ring_position.idl.hh
|
||||
idl/streaming.idl.hh
|
||||
idl/token.idl.hh
|
||||
idl/tracing.idl.hh
|
||||
idl/truncation_record.idl.hh
|
||||
idl/uuid.idl.hh
|
||||
idl/view.idl.hh)
|
||||
|
||||
set(idl_gen_files)
|
||||
|
||||
foreach(f ${idl_serializers})
|
||||
get_filename_component(idl_name "${f}" NAME)
|
||||
get_filename_component(idl_target "${idl_name}" NAME_WE)
|
||||
get_filename_component(idl_dir "${f}" DIRECTORY)
|
||||
string(REPLACE ".idl.hh" ".dist.hh" idl_out_hdr_name "${idl_name}")
|
||||
scylla_generate_idl_serializer(
|
||||
TARGET scylla_idl_gen_${idl_target}
|
||||
VAR scylla_idl_gen_${idl_target}_files
|
||||
IN_FILE ${f}
|
||||
OUT_FILE ${scylla_gen_build_dir}/${idl_dir}/${idl_out_hdr_name})
|
||||
list(APPEND idl_gen_files "${scylla_idl_gen_${idl_target}_files}")
|
||||
endforeach()
|
||||
|
||||
set(scylla_sources
|
||||
absl-flat_hash_map.cc
|
||||
alternator/auth.cc
|
||||
alternator/base64.cc
|
||||
alternator/conditions.cc
|
||||
alternator/executor.cc
|
||||
alternator/expressions.cc
|
||||
alternator/serialization.cc
|
||||
alternator/server.cc
|
||||
alternator/stats.cc
|
||||
alternator/streams.cc
|
||||
api/api.cc
|
||||
api/cache_service.cc
|
||||
api/collectd.cc
|
||||
api/column_family.cc
|
||||
api/commitlog.cc
|
||||
api/compaction_manager.cc
|
||||
api/config.cc
|
||||
api/endpoint_snitch.cc
|
||||
api/error_injection.cc
|
||||
api/failure_detector.cc
|
||||
api/gossiper.cc
|
||||
api/hinted_handoff.cc
|
||||
api/lsa.cc
|
||||
api/messaging_service.cc
|
||||
api/storage_proxy.cc
|
||||
api/storage_service.cc
|
||||
api/stream_manager.cc
|
||||
api/system.cc
|
||||
atomic_cell.cc
|
||||
auth/allow_all_authenticator.cc
|
||||
auth/allow_all_authorizer.cc
|
||||
auth/authenticated_user.cc
|
||||
auth/authentication_options.cc
|
||||
auth/authenticator.cc
|
||||
auth/common.cc
|
||||
auth/default_authorizer.cc
|
||||
auth/password_authenticator.cc
|
||||
auth/passwords.cc
|
||||
auth/permission.cc
|
||||
auth/permissions_cache.cc
|
||||
auth/resource.cc
|
||||
auth/role_or_anonymous.cc
|
||||
auth/roles-metadata.cc
|
||||
auth/sasl_challenge.cc
|
||||
auth/service.cc
|
||||
auth/standard_role_manager.cc
|
||||
auth/transitional.cc
|
||||
bytes.cc
|
||||
canonical_mutation.cc
|
||||
cdc/cdc_partitioner.cc
|
||||
cdc/generation.cc
|
||||
cdc/log.cc
|
||||
cdc/metadata.cc
|
||||
cdc/split.cc
|
||||
clocks-impl.cc
|
||||
collection_mutation.cc
|
||||
compress.cc
|
||||
connection_notifier.cc
|
||||
converting_mutation_partition_applier.cc
|
||||
counters.cc
|
||||
cql3/abstract_marker.cc
|
||||
cql3/attributes.cc
|
||||
cql3/cf_name.cc
|
||||
cql3/column_condition.cc
|
||||
cql3/column_identifier.cc
|
||||
cql3/column_specification.cc
|
||||
cql3/constants.cc
|
||||
cql3/cql3_type.cc
|
||||
cql3/expr/expression.cc
|
||||
cql3/functions/aggregate_fcts.cc
|
||||
cql3/functions/castas_fcts.cc
|
||||
cql3/functions/error_injection_fcts.cc
|
||||
cql3/functions/functions.cc
|
||||
cql3/functions/user_function.cc
|
||||
cql3/index_name.cc
|
||||
cql3/keyspace_element_name.cc
|
||||
cql3/lists.cc
|
||||
cql3/maps.cc
|
||||
cql3/operation.cc
|
||||
cql3/query_options.cc
|
||||
cql3/query_processor.cc
|
||||
cql3/relation.cc
|
||||
cql3/restrictions/statement_restrictions.cc
|
||||
cql3/result_set.cc
|
||||
cql3/role_name.cc
|
||||
cql3/selection/abstract_function_selector.cc
|
||||
cql3/selection/selectable.cc
|
||||
cql3/selection/selection.cc
|
||||
cql3/selection/selector.cc
|
||||
cql3/selection/selector_factories.cc
|
||||
cql3/selection/simple_selector.cc
|
||||
cql3/sets.cc
|
||||
cql3/single_column_relation.cc
|
||||
cql3/statements/alter_keyspace_statement.cc
|
||||
cql3/statements/alter_table_statement.cc
|
||||
cql3/statements/alter_type_statement.cc
|
||||
cql3/statements/alter_view_statement.cc
|
||||
cql3/statements/authentication_statement.cc
|
||||
cql3/statements/authorization_statement.cc
|
||||
cql3/statements/batch_statement.cc
|
||||
cql3/statements/cas_request.cc
|
||||
cql3/statements/cf_prop_defs.cc
|
||||
cql3/statements/cf_statement.cc
|
||||
cql3/statements/create_function_statement.cc
|
||||
cql3/statements/create_index_statement.cc
|
||||
cql3/statements/create_keyspace_statement.cc
|
||||
cql3/statements/create_table_statement.cc
|
||||
cql3/statements/create_type_statement.cc
|
||||
cql3/statements/create_view_statement.cc
|
||||
cql3/statements/delete_statement.cc
|
||||
cql3/statements/drop_function_statement.cc
|
||||
cql3/statements/drop_index_statement.cc
|
||||
cql3/statements/drop_keyspace_statement.cc
|
||||
cql3/statements/drop_table_statement.cc
|
||||
cql3/statements/drop_type_statement.cc
|
||||
cql3/statements/drop_view_statement.cc
|
||||
cql3/statements/function_statement.cc
|
||||
cql3/statements/grant_statement.cc
|
||||
cql3/statements/index_prop_defs.cc
|
||||
cql3/statements/index_target.cc
|
||||
cql3/statements/ks_prop_defs.cc
|
||||
cql3/statements/list_permissions_statement.cc
|
||||
cql3/statements/list_users_statement.cc
|
||||
cql3/statements/modification_statement.cc
|
||||
cql3/statements/permission_altering_statement.cc
|
||||
cql3/statements/property_definitions.cc
|
||||
cql3/statements/raw/parsed_statement.cc
|
||||
cql3/statements/revoke_statement.cc
|
||||
cql3/statements/role-management-statements.cc
|
||||
cql3/statements/schema_altering_statement.cc
|
||||
cql3/statements/select_statement.cc
|
||||
cql3/statements/truncate_statement.cc
|
||||
cql3/statements/update_statement.cc
|
||||
cql3/statements/use_statement.cc
|
||||
cql3/token_relation.cc
|
||||
cql3/tuples.cc
|
||||
cql3/type_json.cc
|
||||
cql3/untyped_result_set.cc
|
||||
cql3/update_parameters.cc
|
||||
cql3/user_types.cc
|
||||
cql3/ut_name.cc
|
||||
cql3/util.cc
|
||||
cql3/values.cc
|
||||
cql3/variable_specifications.cc
|
||||
data/cell.cc
|
||||
database.cc
|
||||
db/batchlog_manager.cc
|
||||
db/commitlog/commitlog.cc
|
||||
db/commitlog/commitlog_entry.cc
|
||||
db/commitlog/commitlog_replayer.cc
|
||||
db/config.cc
|
||||
db/consistency_level.cc
|
||||
db/cql_type_parser.cc
|
||||
db/data_listeners.cc
|
||||
db/extensions.cc
|
||||
db/heat_load_balance.cc
|
||||
db/hints/manager.cc
|
||||
db/hints/resource_manager.cc
|
||||
db/large_data_handler.cc
|
||||
db/legacy_schema_migrator.cc
|
||||
db/marshal/type_parser.cc
|
||||
db/schema_tables.cc
|
||||
db/size_estimates_virtual_reader.cc
|
||||
db/snapshot-ctl.cc
|
||||
db/sstables-format-selector.cc
|
||||
db/system_distributed_keyspace.cc
|
||||
db/system_keyspace.cc
|
||||
db/view/row_locking.cc
|
||||
db/view/view.cc
|
||||
db/view/view_update_generator.cc
|
||||
dht/boot_strapper.cc
|
||||
dht/i_partitioner.cc
|
||||
dht/murmur3_partitioner.cc
|
||||
dht/range_streamer.cc
|
||||
dht/token.cc
|
||||
distributed_loader.cc
|
||||
duration.cc
|
||||
exceptions/exceptions.cc
|
||||
flat_mutation_reader.cc
|
||||
frozen_mutation.cc
|
||||
frozen_schema.cc
|
||||
gms/application_state.cc
|
||||
gms/endpoint_state.cc
|
||||
gms/failure_detector.cc
|
||||
gms/feature_service.cc
|
||||
gms/gossip_digest_ack.cc
|
||||
gms/gossip_digest_ack2.cc
|
||||
gms/gossip_digest_syn.cc
|
||||
gms/gossiper.cc
|
||||
gms/inet_address.cc
|
||||
gms/version_generator.cc
|
||||
gms/versioned_value.cc
|
||||
hashers.cc
|
||||
index/secondary_index.cc
|
||||
index/secondary_index_manager.cc
|
||||
init.cc
|
||||
keys.cc
|
||||
lister.cc
|
||||
locator/abstract_replication_strategy.cc
|
||||
locator/ec2_multi_region_snitch.cc
|
||||
locator/ec2_snitch.cc
|
||||
locator/everywhere_replication_strategy.cc
|
||||
locator/gce_snitch.cc
|
||||
locator/gossiping_property_file_snitch.cc
|
||||
locator/local_strategy.cc
|
||||
locator/network_topology_strategy.cc
|
||||
locator/production_snitch_base.cc
|
||||
locator/rack_inferring_snitch.cc
|
||||
locator/simple_snitch.cc
|
||||
locator/simple_strategy.cc
|
||||
locator/snitch_base.cc
|
||||
locator/token_metadata.cc
|
||||
lua.cc
|
||||
main.cc
|
||||
memtable.cc
|
||||
message/messaging_service.cc
|
||||
multishard_mutation_query.cc
|
||||
mutation.cc
|
||||
raft/fsm.cc
|
||||
raft/log.cc
|
||||
raft/progress.cc
|
||||
raft/raft.cc
|
||||
raft/server.cc
|
||||
mutation_fragment.cc
|
||||
mutation_partition.cc
|
||||
mutation_partition_serializer.cc
|
||||
mutation_partition_view.cc
|
||||
mutation_query.cc
|
||||
mutation_reader.cc
|
||||
mutation_writer/multishard_writer.cc
|
||||
mutation_writer/shard_based_splitting_writer.cc
|
||||
mutation_writer/timestamp_based_splitting_writer.cc
|
||||
partition_slice_builder.cc
|
||||
partition_version.cc
|
||||
querier.cc
|
||||
query-result-set.cc
|
||||
query.cc
|
||||
range_tombstone.cc
|
||||
range_tombstone_list.cc
|
||||
reader_concurrency_semaphore.cc
|
||||
redis/abstract_command.cc
|
||||
redis/command_factory.cc
|
||||
redis/commands.cc
|
||||
redis/keyspace_utils.cc
|
||||
redis/lolwut.cc
|
||||
redis/mutation_utils.cc
|
||||
redis/options.cc
|
||||
redis/query_processor.cc
|
||||
redis/query_utils.cc
|
||||
redis/server.cc
|
||||
redis/service.cc
|
||||
redis/stats.cc
|
||||
repair/repair.cc
|
||||
repair/row_level.cc
|
||||
row_cache.cc
|
||||
schema.cc
|
||||
schema_mutations.cc
|
||||
schema_registry.cc
|
||||
service/client_state.cc
|
||||
service/migration_manager.cc
|
||||
service/migration_task.cc
|
||||
service/misc_services.cc
|
||||
service/pager/paging_state.cc
|
||||
service/pager/query_pagers.cc
|
||||
service/paxos/paxos_state.cc
|
||||
service/paxos/prepare_response.cc
|
||||
service/paxos/prepare_summary.cc
|
||||
service/paxos/proposal.cc
|
||||
service/priority_manager.cc
|
||||
service/storage_proxy.cc
|
||||
service/storage_service.cc
|
||||
sstables/compaction.cc
|
||||
sstables/compaction_manager.cc
|
||||
sstables/compaction_strategy.cc
|
||||
sstables/compress.cc
|
||||
sstables/integrity_checked_file_impl.cc
|
||||
sstables/kl/writer.cc
|
||||
sstables/leveled_compaction_strategy.cc
|
||||
sstables/m_format_read_helpers.cc
|
||||
sstables/metadata_collector.cc
|
||||
sstables/mp_row_consumer.cc
|
||||
sstables/mx/writer.cc
|
||||
sstables/partition.cc
|
||||
sstables/prepended_input_stream.cc
|
||||
sstables/random_access_reader.cc
|
||||
sstables/size_tiered_compaction_strategy.cc
|
||||
sstables/sstable_directory.cc
|
||||
sstables/sstable_version.cc
|
||||
sstables/sstables.cc
|
||||
sstables/sstables_manager.cc
|
||||
sstables/time_window_compaction_strategy.cc
|
||||
sstables/writer.cc
|
||||
streaming/progress_info.cc
|
||||
streaming/session_info.cc
|
||||
streaming/stream_coordinator.cc
|
||||
streaming/stream_manager.cc
|
||||
streaming/stream_plan.cc
|
||||
streaming/stream_reason.cc
|
||||
streaming/stream_receive_task.cc
|
||||
streaming/stream_request.cc
|
||||
streaming/stream_result_future.cc
|
||||
streaming/stream_session.cc
|
||||
streaming/stream_session_state.cc
|
||||
streaming/stream_summary.cc
|
||||
streaming/stream_task.cc
|
||||
streaming/stream_transfer_task.cc
|
||||
table.cc
|
||||
table_helper.cc
|
||||
thrift/controller.cc
|
||||
thrift/handler.cc
|
||||
thrift/server.cc
|
||||
thrift/thrift_validation.cc
|
||||
timeout_config.cc
|
||||
tracing/trace_keyspace_helper.cc
|
||||
tracing/trace_state.cc
|
||||
tracing/traced_file.cc
|
||||
tracing/tracing.cc
|
||||
tracing/tracing_backend_registry.cc
|
||||
transport/controller.cc
|
||||
transport/cql_protocol_extension.cc
|
||||
transport/event.cc
|
||||
transport/event_notifier.cc
|
||||
transport/messages/result_message.cc
|
||||
transport/server.cc
|
||||
types.cc
|
||||
unimplemented.cc
|
||||
utils/UUID_gen.cc
|
||||
utils/arch/powerpc/crc32-vpmsum/crc32_wrapper.cc
|
||||
utils/array-search.cc
|
||||
utils/ascii.cc
|
||||
utils/big_decimal.cc
|
||||
utils/bloom_calculations.cc
|
||||
utils/bloom_filter.cc
|
||||
utils/buffer_input_stream.cc
|
||||
utils/build_id.cc
|
||||
utils/config_file.cc
|
||||
utils/directories.cc
|
||||
utils/disk-error-handler.cc
|
||||
utils/dynamic_bitset.cc
|
||||
utils/error_injection.cc
|
||||
utils/exceptions.cc
|
||||
utils/file_lock.cc
|
||||
utils/generation-number.cc
|
||||
utils/gz/crc_combine.cc
|
||||
utils/human_readable.cc
|
||||
utils/i_filter.cc
|
||||
utils/large_bitset.cc
|
||||
utils/like_matcher.cc
|
||||
utils/limiting_data_source.cc
|
||||
utils/logalloc.cc
|
||||
utils/managed_bytes.cc
|
||||
utils/multiprecision_int.cc
|
||||
utils/murmur_hash.cc
|
||||
utils/rate_limiter.cc
|
||||
utils/rjson.cc
|
||||
utils/runtime.cc
|
||||
utils/updateable_value.cc
|
||||
utils/utf8.cc
|
||||
utils/uuid.cc
|
||||
validation.cc
|
||||
vint-serialization.cc
|
||||
zstd.cc
|
||||
release.cc)
|
||||
|
||||
set(scylla_gen_sources
|
||||
"${scylla_thrift_gen_cassandra_files}"
|
||||
"${scylla_ragel_gen_protocol_parser_file}"
|
||||
"${swagger_gen_files}"
|
||||
"${idl_gen_files}"
|
||||
"${antlr3_gen_files}")
|
||||
set(SCYLLA_SOURCE_FILES
|
||||
${SCYLLA_ROOT_SOURCE_FILES}
|
||||
${SCYLLA_GEN_SOURCE_FILES}
|
||||
${SCYLLA_SUB_SOURCE_FILES})
|
||||
|
||||
add_executable(scylla
|
||||
${scylla_sources}
|
||||
${scylla_gen_sources})
|
||||
${SEASTAR_SOURCE_FILES}
|
||||
${SCYLLA_SOURCE_FILES})
|
||||
|
||||
target_link_libraries(scylla PRIVATE
|
||||
seastar
|
||||
# Boost dependencies
|
||||
Boost::filesystem
|
||||
Boost::program_options
|
||||
Boost::system
|
||||
Boost::thread
|
||||
Boost::regex
|
||||
Boost::headers
|
||||
# Abseil libs
|
||||
absl::hashtablez_sampler
|
||||
absl::raw_hash_set
|
||||
absl::synchronization
|
||||
absl::graphcycles_internal
|
||||
absl::stacktrace
|
||||
absl::symbolize
|
||||
absl::debugging_internal
|
||||
absl::demangle_internal
|
||||
absl::time
|
||||
absl::time_zone
|
||||
absl::int128
|
||||
absl::city
|
||||
absl::hash
|
||||
absl::malloc_internal
|
||||
absl::spinlock_wait
|
||||
absl::base
|
||||
absl::dynamic_annotations
|
||||
absl::raw_logging_internal
|
||||
absl::exponential_biased
|
||||
absl::throw_delegate
|
||||
# System libs
|
||||
ZLIB::ZLIB
|
||||
ICU::uc
|
||||
systemd
|
||||
zstd
|
||||
snappy
|
||||
${LUA_LIBRARIES}
|
||||
thrift
|
||||
crypt)
|
||||
# Note that since CLion does not undestand GCC6 concepts, we always disable them (even if users configure otherwise).
|
||||
# CLion seems to have trouble with `-U` (macro undefinition), so we do it this way instead.
|
||||
list(REMOVE_ITEM SEASTAR_CFLAGS "-DHAVE_GCC6_CONCEPTS")
|
||||
|
||||
target_link_libraries(scylla PRIVATE
|
||||
-Wl,--build-id=sha1 # Force SHA1 build-id generation
|
||||
# TODO: Use lld linker if it's available, otherwise gold, else bfd
|
||||
-fuse-ld=lld)
|
||||
# TODO: patch dynamic linker to match configure.py behavior
|
||||
# If the Seastar pkg-config information is available, append to the default flags.
|
||||
#
|
||||
# For ease of browsing the source code, we always pretend that DPDK is enabled.
|
||||
target_compile_options(scylla PUBLIC
|
||||
-std=gnu++1z
|
||||
-DHAVE_DPDK
|
||||
-DHAVE_HWLOC
|
||||
"${SEASTAR_CFLAGS}")
|
||||
|
||||
target_compile_options(scylla PRIVATE
|
||||
-std=gnu++20
|
||||
-fcoroutines # TODO: Clang does not have this flag, adjust to both variants
|
||||
${target_arch_flag})
|
||||
# Hacks needed to expose internal APIs for xxhash dependencies
|
||||
target_compile_definitions(scylla PRIVATE XXH_PRIVATE_API HAVE_LZ4_COMPRESS_DEFAULT)
|
||||
|
||||
target_include_directories(scylla PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
libdeflate
|
||||
abseil
|
||||
"${scylla_gen_build_dir}")
|
||||
|
||||
###
|
||||
### Create crc_combine_table helper executable.
|
||||
### Use it to generate crc_combine_table.cc to be used in scylla at build time.
|
||||
###
|
||||
add_executable(crc_combine_table utils/gz/gen_crc_combine_table.cc)
|
||||
target_link_libraries(crc_combine_table PRIVATE seastar)
|
||||
target_include_directories(crc_combine_table PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
target_compile_options(crc_combine_table PRIVATE
|
||||
-std=gnu++20
|
||||
-fcoroutines
|
||||
${target_arch_flag})
|
||||
add_dependencies(scylla crc_combine_table)
|
||||
|
||||
# Generate an additional source file at build time that is needed for Scylla compilation
|
||||
add_custom_command(OUTPUT "${scylla_gen_build_dir}/utils/gz/crc_combine_table.cc"
|
||||
COMMAND $<TARGET_FILE:crc_combine_table> > "${scylla_gen_build_dir}/utils/gz/crc_combine_table.cc"
|
||||
DEPENDS crc_combine_table)
|
||||
target_sources(scylla PRIVATE "${scylla_gen_build_dir}/utils/gz/crc_combine_table.cc")
|
||||
|
||||
###
|
||||
### Generate version file and supply appropriate compile definitions for release.cc
|
||||
###
|
||||
execute_process(COMMAND ${CMAKE_SOURCE_DIR}/SCYLLA-VERSION-GEN RESULT_VARIABLE scylla_version_gen_res)
|
||||
if(scylla_version_gen_res)
|
||||
message(SEND_ERROR "Version file generation failed. Return code: ${scylla_version_gen_res}")
|
||||
endif()
|
||||
|
||||
file(READ build/SCYLLA-VERSION-FILE scylla_version)
|
||||
string(STRIP "${scylla_version}" scylla_version)
|
||||
|
||||
file(READ build/SCYLLA-RELEASE-FILE scylla_release)
|
||||
string(STRIP "${scylla_release}" scylla_release)
|
||||
|
||||
get_property(release_cdefs SOURCE "${CMAKE_SOURCE_DIR}/release.cc" PROPERTY COMPILE_DEFINITIONS)
|
||||
list(APPEND release_cdefs "SCYLLA_VERSION=\"${scylla_version}\"" "SCYLLA_RELEASE=\"${scylla_release}\"")
|
||||
set_source_files_properties("${CMAKE_SOURCE_DIR}/release.cc" PROPERTIES COMPILE_DEFINITIONS "${release_cdefs}")
|
||||
|
||||
###
|
||||
### Custom command for building libdeflate. Link the library to scylla.
|
||||
###
|
||||
set(libdeflate_lib "${scylla_build_dir}/libdeflate/libdeflate.a")
|
||||
add_custom_command(OUTPUT "${libdeflate_lib}"
|
||||
COMMAND make -C libdeflate
|
||||
BUILD_DIR=../build/${BUILD_TYPE}/libdeflate/
|
||||
CC=${CMAKE_C_COMPILER}
|
||||
"CFLAGS=${target_arch_flag}"
|
||||
../build/${BUILD_TYPE}/libdeflate//libdeflate.a) # Two backslashes are important!
|
||||
# Hack to force generating custom command to produce libdeflate.a
|
||||
add_custom_target(libdeflate DEPENDS "${libdeflate_lib}")
|
||||
target_link_libraries(scylla PRIVATE "${libdeflate_lib}")
|
||||
|
||||
# TODO: create cmake/ directory and move utilities (generate functions etc) there
|
||||
# TODO: Build tests if BUILD_TESTING=on (using CTest module)
|
||||
# The order matters here: prefer the "static" DPDK directories to any dynamic paths from pkg-config. Some files are only
|
||||
# available dynamically, though.
|
||||
target_include_directories(scylla PUBLIC
|
||||
.
|
||||
${SEASTAR_DPDK_INCLUDE_DIRS}
|
||||
${SEASTAR_INCLUDE_DIRS}
|
||||
${Boost_INCLUDE_DIRS}
|
||||
xxhash
|
||||
libdeflate
|
||||
build/release/gen)
|
||||
|
||||
@@ -8,4 +8,4 @@ Please use the [Issue Tracker](https://github.com/scylladb/scylla/issues/) to re
|
||||
|
||||
# Contributing Code to Scylla
|
||||
|
||||
To contribute code to Scylla, you need to sign the [Contributor License Agreement](https://www.scylladb.com/open-source/contributor-agreement/) and send your changes as [patches](https://github.com/scylladb/scylla/wiki/Formatting-and-sending-patches) to the [mailing list](https://groups.google.com/forum/#!forum/scylladb-dev). We don't accept pull requests on GitHub.
|
||||
To contribute code to Scylla, you need to sign the [Contributor License Agreement](http://www.scylladb.com/opensource/cla/) and send your changes as [patches](https://github.com/scylladb/scylla/wiki/Formatting-and-sending-patches) to the [mailing list](https://groups.google.com/forum/#!forum/scylladb-dev). We don't accept pull requests on GitHub.
|
||||
|
||||
32
HACKING.md
32
HACKING.md
@@ -18,35 +18,23 @@ $ git submodule update --init --recursive
|
||||
|
||||
### Dependencies
|
||||
|
||||
Scylla is fairly fussy about its build environment, requiring a very recent
|
||||
version of the C++20 compiler and numerous tools and libraries to build.
|
||||
Scylla depends on the system package manager for its development dependencies.
|
||||
|
||||
Run `./install-dependencies.sh` (as root) to use your Linux distributions's
|
||||
package manager to install the appropriate packages on your build machine.
|
||||
However, this will only work on very recent distributions. For example,
|
||||
currently Fedora users must upgrade to Fedora 32 otherwise the C++ compiler
|
||||
will be too old, and not support the new C++20 standard that Scylla uses.
|
||||
Running `./install-dependencies.sh` (as root) installs the appropriate packages based on your Linux distribution.
|
||||
|
||||
Alternatively, to avoid having to upgrade your build machine or install
|
||||
various packages on it, we provide another option - the **frozen toolchain**.
|
||||
This is a script, `./tools/toolchain/dbuild`, that can execute build or run
|
||||
commands inside a Docker image that contains exactly the right build tools and
|
||||
libraries. The `dbuild` technique is useful for beginners, but is also the way
|
||||
in which ScyllaDB produces official releases, so it is highly recommended.
|
||||
On Ubuntu and Debian based Linux distributions, some packages
|
||||
required to build Scylla are missing in the official upstream:
|
||||
|
||||
To use `dbuild`, you simply prefix any build or run command with it. Building
|
||||
and running Scylla becomes as easy as:
|
||||
- libthrift-dev and libthrift
|
||||
- antlr3-c++-dev
|
||||
|
||||
```bash
|
||||
$ ./tools/toolchain/dbuild ./configure.py
|
||||
$ ./tools/toolchain/dbuild ninja build/release/scylla
|
||||
$ ./tools/toolchain/dbuild ./build/release/scylla --developer-mode 1
|
||||
```
|
||||
Try running ```sudo ./scripts/scylla_current_repo``` to add Scylla upstream,
|
||||
and get the missing packages from it.
|
||||
|
||||
### Build system
|
||||
|
||||
**Note**: Compiling Scylla requires, conservatively, 2 GB of memory per native
|
||||
thread, and up to 3 GB per native thread while linking. GCC >= 10 is
|
||||
thread, and up to 3 GB per native thread while linking. GCC >= 8.1.1. is
|
||||
required.
|
||||
|
||||
Scylla is built with [Ninja](https://ninja-build.org/), a low-level rule-based system. A Python script, `configure.py`, generates a Ninja file (`build.ninja`) based on configuration options.
|
||||
@@ -153,7 +141,7 @@ In v3:
|
||||
"Tests: unit ({mode}), dtest ({smp})"
|
||||
```
|
||||
|
||||
The usual is "Tests: unit (dev)", although running debug tests is encouraged.
|
||||
The usual is "Tests: unit (release)", although running debug tests is encouraged.
|
||||
|
||||
5. When answering review comments, prefer inline quotes as they make it easier to track the conversation across multiple e-mails.
|
||||
|
||||
|
||||
114
MAINTAINERS
Normal file
114
MAINTAINERS
Normal file
@@ -0,0 +1,114 @@
|
||||
M: Maintainer with commit access
|
||||
R: Reviewer with subsystem expertise
|
||||
F: Filename, directory, or pattern for the subsystem
|
||||
|
||||
---
|
||||
|
||||
AUTH
|
||||
R: Calle Wilund <calle@scylladb.com>
|
||||
R: Vlad Zolotarov <vladz@scylladb.com>
|
||||
R: Jesse Haber-Kucharsky <jhaberku@scylladb.com>
|
||||
F: auth/*
|
||||
|
||||
CACHE
|
||||
M: Tomasz Grabiec <tgrabiec@scylladb.com>
|
||||
R: Piotr Jastrzebski <piotr@scylladb.com>
|
||||
F: row_cache*
|
||||
F: *mutation*
|
||||
F: tests/mvcc*
|
||||
|
||||
COMMITLOG / BATCHLOGa
|
||||
R: Calle Wilund <calle@scylladb.com>
|
||||
F: db/commitlog/*
|
||||
F: db/batch*
|
||||
|
||||
COORDINATOR
|
||||
R: Gleb Natapov <gleb@scylladb.com>
|
||||
F: service/storage_proxy*
|
||||
|
||||
COMPACTION
|
||||
R: Raphael S. Carvalho <raphaelsc@scylladb.com>
|
||||
R: Glauber Costa <glauber@scylladb.com>
|
||||
R: Nadav Har'El <nyh@scylladb.com>
|
||||
F: sstables/compaction*
|
||||
|
||||
CQL TRANSPORT LAYER
|
||||
M: Pekka Enberg <penberg@scylladb.com>
|
||||
F: transport/*
|
||||
|
||||
CQL QUERY LANGUAGE
|
||||
M: Tomasz Grabiec <tgrabiec@scylladb.com>
|
||||
M: Pekka Enberg <penberg@scylladb.com>
|
||||
F: cql3/*
|
||||
|
||||
COUNTERS
|
||||
F: counters*
|
||||
F: tests/counter_test*
|
||||
|
||||
GOSSIP
|
||||
M: Tomasz Grabiec <tgrabiec@scylladb.com>
|
||||
R: Asias He <asias@scylladb.com>
|
||||
F: gms/*
|
||||
|
||||
DOCKER
|
||||
M: Pekka Enberg <penberg@scylladb.com>
|
||||
F: dist/docker/*
|
||||
|
||||
LSA
|
||||
M: Tomasz Grabiec <tgrabiec@scylladb.com>
|
||||
F: utils/logalloc*
|
||||
|
||||
MATERIALIZED VIEWS
|
||||
M: Pekka Enberg <penberg@scylladb.com>
|
||||
M: Nadav Har'El <nyh@scylladb.com>
|
||||
F: db/view/*
|
||||
F: cql3/statements/*view*
|
||||
|
||||
PACKAGING
|
||||
R: Takuya ASADA <syuu@scylladb.com>
|
||||
F: dist/*
|
||||
|
||||
REPAIR
|
||||
M: Tomasz Grabiec <tgrabiec@scylladb.com>
|
||||
R: Asias He <asias@scylladb.com>
|
||||
R: Nadav Har'El <nyh@scylladb.com>
|
||||
F: repair/*
|
||||
|
||||
SCHEMA MANAGEMENT
|
||||
M: Tomasz Grabiec <tgrabiec@scylladb.com>
|
||||
M: Pekka Enberg <penberg@scylladb.com>
|
||||
F: db/schema_tables*
|
||||
F: db/legacy_schema_migrator*
|
||||
F: service/migration*
|
||||
F: schema*
|
||||
|
||||
SECONDARY INDEXES
|
||||
M: Pekka Enberg <penberg@scylladb.com>
|
||||
M: Nadav Har'El <nyh@scylladb.com>
|
||||
R: Pekka Enberg <penberg@scylladb.com>
|
||||
F: db/index/*
|
||||
F: cql3/statements/*index*
|
||||
|
||||
SSTABLES
|
||||
M: Tomasz Grabiec <tgrabiec@scylladb.com>
|
||||
R: Raphael S. Carvalho <raphaelsc@scylladb.com>
|
||||
R: Glauber Costa <glauber@scylladb.com>
|
||||
R: Nadav Har'El <nyh@scylladb.com>
|
||||
F: sstables/*
|
||||
|
||||
STREAMING
|
||||
M: Tomasz Grabiec <tgrabiec@scylladb.com>
|
||||
R: Asias He <asias@scylladb.com>
|
||||
F: streaming/*
|
||||
F: service/storage_service.*
|
||||
|
||||
ALTERNATOR
|
||||
M: Nadav Har'El <nyh@scylladb.com>
|
||||
F: alternator/*
|
||||
F: alternator-test/*
|
||||
|
||||
THE REST
|
||||
M: Avi Kivity <avi@scylladb.com>
|
||||
M: Tomasz Grabiec <tgrabiec@scylladb.com>
|
||||
M: Nadav Har'El <nyh@scylladb.com>
|
||||
F: *
|
||||
@@ -1,7 +1,5 @@
|
||||
This project includes code developed by the Apache Software Foundation (http://www.apache.org/),
|
||||
especially Apache Cassandra.
|
||||
|
||||
It includes files from https://github.com/antonblanchard/crc32-vpmsum (author Anton Blanchard <anton@au.ibm.com>, IBM).
|
||||
It also includes files from https://github.com/antonblanchard/crc32-vpmsum (author Anton Blanchard <anton@au.ibm.com>, IBM).
|
||||
These files are located in utils/arch/powerpc/crc32-vpmsum. Their license may be found in licenses/LICENSE-crc32-vpmsum.TXT.
|
||||
|
||||
It includes modified code from https://gitbox.apache.org/repos/asf?p=cassandra-dtest.git (owned by The Apache Software Foundation)
|
||||
|
||||
146
README.md
146
README.md
@@ -1,78 +1,49 @@
|
||||
# Scylla
|
||||
|
||||
[](http://slack.scylladb.com)
|
||||
[](https://twitter.com/intent/follow?screen_name=ScyllaDB)
|
||||
## Quick-start
|
||||
|
||||
## What is Scylla?
|
||||
|
||||
Scylla is the real-time big data database that is API-compatible with Apache Cassandra and Amazon DynamoDB.
|
||||
Scylla embraces a shared-nothing approach that increases throughput and storage capacity to realize order-of-magnitude performance improvements and reduce hardware costs.
|
||||
|
||||
For more information, please see the [ScyllaDB web site].
|
||||
|
||||
[ScyllaDB web site]: https://www.scylladb.com
|
||||
|
||||
## Build Prerequisites
|
||||
|
||||
Scylla is fairly fussy about its build environment, requiring very recent
|
||||
versions of the C++20 compiler and of many libraries to build. The document
|
||||
[HACKING.md](HACKING.md) includes detailed information on building and
|
||||
developing Scylla, but to get Scylla building quickly on (almost) any build
|
||||
machine, Scylla offers a [frozen toolchain](tools/toolchain/README.md),
|
||||
This is a pre-configured Docker image which includes recent versions of all
|
||||
the required compilers, libraries and build tools. Using the frozen toolchain
|
||||
allows you to avoid changing anything in your build machine to meet Scylla's
|
||||
requirements - you just need to meet the frozen toolchain's prerequisites
|
||||
(mostly, Docker or Podman being available).
|
||||
|
||||
## Building Scylla
|
||||
|
||||
Building Scylla with the frozen toolchain `dbuild` is as easy as:
|
||||
To get the build going quickly, Scylla offers a [frozen toolchain](tools/toolchain/README.md)
|
||||
which would build and run Scylla using a pre-configured Docker image.
|
||||
Using the frozen toolchain will also isolate all of the installed
|
||||
dependencies in a Docker container.
|
||||
Assuming you have met the toolchain prerequisites, which is running
|
||||
Docker in user mode, building and running is as easy as:
|
||||
|
||||
```bash
|
||||
$ git submodule update --init --force --recursive
|
||||
$ ./tools/toolchain/dbuild ./configure.py
|
||||
$ ./tools/toolchain/dbuild ninja build/release/scylla
|
||||
```
|
||||
$ ./tools/toolchain/dbuild ./configure.py
|
||||
$ ./tools/toolchain/dbuild ninja build/release/scylla
|
||||
$ ./tools/toolchain/dbuild ./build/release/scylla --developer-mode 1
|
||||
```
|
||||
|
||||
For further information, please see:
|
||||
Please see [HACKING.md](HACKING.md) for detailed information on building and developing Scylla.
|
||||
|
||||
* [Developer documentation] for more information on building Scylla.
|
||||
* [Build documentation] on how to build Scylla binaries, tests, and packages.
|
||||
* [Docker image build documentation] for information on how to build Docker images.
|
||||
|
||||
[developer documentation]: HACKING.md
|
||||
[build documentation]: docs/building.md
|
||||
[docker image build documentation]: dist/docker/redhat/README.md
|
||||
**Note**: GCC >= 8.1.1 is required to compile Scylla.
|
||||
|
||||
## Running Scylla
|
||||
|
||||
To start Scylla server, run:
|
||||
* Run Scylla
|
||||
```
|
||||
./build/release/scylla
|
||||
|
||||
```bash
|
||||
$ ./tools/toolchain/dbuild ./build/release/scylla --workdir tmp --smp 1 --developer-mode 1
|
||||
```
|
||||
|
||||
This will start a Scylla node with one CPU core allocated to it and data files stored in the `tmp` directory.
|
||||
The `--developer-mode` is needed to disable the various checks Scylla performs at startup to ensure the machine is configured for maximum performance (not relevant on development workstations).
|
||||
Please note that you need to run Scylla with `dbuild` if you built it with the frozen toolchain.
|
||||
* run Scylla with one CPU and ./tmp as work directory
|
||||
|
||||
For more run options, run:
|
||||
|
||||
```bash
|
||||
$ ./tools/toolchain/dbuild ./build/release/scylla --help
|
||||
```
|
||||
./build/release/scylla --workdir tmp --smp 1
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
See [test.py manual](docs/testing.md).
|
||||
* For more run options:
|
||||
```
|
||||
./build/release/scylla --help
|
||||
```
|
||||
|
||||
## Scylla APIs and compatibility
|
||||
By default, Scylla is compatible with Apache Cassandra and its APIs - CQL and
|
||||
Thrift. There is also support for the API of Amazon DynamoDB™,
|
||||
which needs to be enabled and configured in order to be used. For more
|
||||
information on how to enable the DynamoDB™ API in Scylla,
|
||||
and the current compatibility of this feature as well as Scylla-specific extensions, see
|
||||
Thrift. There is also experimental support for the API of Amazon DynamoDB,
|
||||
but being experimental it needs to be explicitly enabled to be used. For more
|
||||
information on how to enable the experimental DynamoDB compatibility in Scylla,
|
||||
and the current limitations of this feature, see
|
||||
[Alternator](docs/alternator/alternator.md) and
|
||||
[Getting started with Alternator](docs/alternator/getting-started.md).
|
||||
|
||||
@@ -85,29 +56,48 @@ both.
|
||||
Seastar documentation can be found [here](http://docs.seastar.io/master/index.html).
|
||||
User documentation can be found [here](https://docs.scylladb.com/).
|
||||
|
||||
## Training
|
||||
## Building Fedora RPM
|
||||
|
||||
Training material and online courses can be found at [Scylla University](https://university.scylladb.com/).
|
||||
The courses are free, self-paced and include hands-on examples. They cover a variety of topics including Scylla data modeling,
|
||||
administration, architecture, basic NoSQL concepts, using drivers for application development, Scylla setup, failover, compactions,
|
||||
multi-datacenters and how Scylla integrates with third-party applications.
|
||||
As a pre-requisite, you need to install [Mock](https://fedoraproject.org/wiki/Mock) on your machine:
|
||||
|
||||
```
|
||||
# Install mock:
|
||||
sudo yum install mock
|
||||
|
||||
# Add user to the "mock" group:
|
||||
usermod -a -G mock $USER && newgrp mock
|
||||
```
|
||||
|
||||
Then, to build an RPM, run:
|
||||
|
||||
```
|
||||
./dist/redhat/build_rpm.sh
|
||||
```
|
||||
|
||||
The built RPM is stored in ``/var/lib/mock/<configuration>/result`` directory.
|
||||
For example, on Fedora 21 mock reports the following:
|
||||
|
||||
```
|
||||
INFO: Done(scylla-server-0.00-1.fc21.src.rpm) Config(default) 20 minutes 7 seconds
|
||||
INFO: Results and/or logs in: /var/lib/mock/fedora-21-x86_64/result
|
||||
```
|
||||
|
||||
## Building Fedora-based Docker image
|
||||
|
||||
Build a Docker image with:
|
||||
|
||||
```
|
||||
cd dist/docker
|
||||
docker build -t <image-name> .
|
||||
```
|
||||
|
||||
Run the image with:
|
||||
|
||||
```
|
||||
docker run -p $(hostname -i):9042:9042 -i -t <image name>
|
||||
```
|
||||
|
||||
## Contributing to Scylla
|
||||
|
||||
If you want to report a bug or submit a pull request or a patch, please read the [contribution guidelines].
|
||||
|
||||
If you are a developer working on Scylla, please read the [developer guidelines].
|
||||
|
||||
[contribution guidelines]: CONTRIBUTING.md
|
||||
[developer guidelines]: HACKING.md
|
||||
|
||||
## Contact
|
||||
|
||||
* The [users mailing list] and [Slack channel] are for users to discuss configuration, management, and operations of the ScyllaDB open source.
|
||||
* The [developers mailing list] is for developers and people interested in following the development of ScyllaDB to discuss technical topics.
|
||||
|
||||
[Users mailing list]: https://groups.google.com/forum/#!forum/scylladb-users
|
||||
|
||||
[Slack channel]: http://slack.scylladb.com/
|
||||
|
||||
[Developers mailing list]: https://groups.google.com/forum/#!forum/scylladb-dev
|
||||
[Hacking howto](HACKING.md)
|
||||
[Guidelines for contributing](CONTRIBUTING.md)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
PRODUCT=scylla
|
||||
VERSION=4.4.dev
|
||||
VERSION=3.3.4
|
||||
|
||||
if test -f version
|
||||
then
|
||||
@@ -19,14 +19,6 @@ else
|
||||
SCYLLA_RELEASE=$SCYLLA_BUILD.$DATE.$GIT_COMMIT
|
||||
fi
|
||||
|
||||
if [ -f build/SCYLLA-RELEASE-FILE ]; then
|
||||
RELEASE_FILE=$(cat build/SCYLLA-RELEASE-FILE)
|
||||
GIT_COMMIT_FILE=$(cat build/SCYLLA-RELEASE-FILE |cut -d . -f 3)
|
||||
if [ "$GIT_COMMIT" = "$GIT_COMMIT_FILE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$SCYLLA_VERSION-$SCYLLA_RELEASE"
|
||||
mkdir -p build
|
||||
echo "$SCYLLA_VERSION" > build/SCYLLA-VERSION-FILE
|
||||
|
||||
1
abseil
1
abseil
Submodule abseil deleted from 1e3d25b265
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla 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 Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "absl-flat_hash_map.hh"
|
||||
|
||||
size_t sstring_hash::operator()(std::string_view v) const noexcept {
|
||||
return absl::Hash<std::string_view>{}(v);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla 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 Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <absl/container/flat_hash_map.h>
|
||||
#include <seastar/core/sstring.hh>
|
||||
|
||||
using namespace seastar;
|
||||
|
||||
struct sstring_hash {
|
||||
using is_transparent = void;
|
||||
size_t operator()(std::string_view v) const noexcept;
|
||||
};
|
||||
|
||||
struct sstring_eq {
|
||||
using is_transparent = void;
|
||||
bool operator()(std::string_view a, std::string_view b) const noexcept {
|
||||
return a == b;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename K, typename V, typename... Ts>
|
||||
struct flat_hash_map : public absl::flat_hash_map<K, V, Ts...> {
|
||||
};
|
||||
|
||||
template <typename V>
|
||||
struct flat_hash_map<sstring, V>
|
||||
: public absl::flat_hash_map<sstring, V, sstring_hash, sstring_eq> {};
|
||||
@@ -26,14 +26,6 @@ import pytest
|
||||
import boto3
|
||||
from util import create_test_table
|
||||
|
||||
# When tests are run with HTTPS, the server often won't have its SSL
|
||||
# certificate signed by a known authority. So we will disable certificate
|
||||
# verification with the "verify=False" request option. However, once we do
|
||||
# that, we start getting scary-looking warning messages, saying that this
|
||||
# makes HTTPS insecure. The following silences those warnings:
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
# Test that the Boto libraries are new enough. These tests want to test a
|
||||
# large variety of DynamoDB API features, and to do this we need a new-enough
|
||||
# version of the the Boto libraries (boto3 and botocore) so that they can
|
||||
@@ -54,8 +46,6 @@ def pytest_addoption(parser):
|
||||
parser.addoption("--https", action="store_true",
|
||||
help="communicate via HTTPS protocol on port 8043 instead of HTTP when"
|
||||
" running against a local Scylla installation")
|
||||
parser.addoption("--url", action="store",
|
||||
help="communicate with given URL instead of defaults")
|
||||
|
||||
# "dynamodb" fixture: set up client object for communicating with the DynamoDB
|
||||
# API. Currently this chooses either Amazon's DynamoDB in the default region
|
||||
@@ -72,34 +62,15 @@ def dynamodb(request):
|
||||
# requires us to specify dummy region and credential parameters,
|
||||
# otherwise the user is forced to properly configure ~/.aws even
|
||||
# for local runs.
|
||||
if request.config.getoption('url') != None:
|
||||
local_url = request.config.getoption('url')
|
||||
else:
|
||||
local_url = 'https://localhost:8043' if request.config.getoption('https') else 'http://localhost:8000'
|
||||
local_url = 'https://localhost:8043' if request.config.getoption('https') else 'http://localhost:8000'
|
||||
# Disable verifying in order to be able to use self-signed TLS certificates
|
||||
verify = not request.config.getoption('https')
|
||||
# Silencing the 'Unverified HTTPS request warning'
|
||||
if request.config.getoption('https'):
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
return boto3.resource('dynamodb', endpoint_url=local_url, verify=verify,
|
||||
region_name='us-east-1', aws_access_key_id='alternator', aws_secret_access_key='secret_pass',
|
||||
config=botocore.client.Config(retries={"max_attempts": 3}))
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def dynamodbstreams(request):
|
||||
if request.config.getoption('aws'):
|
||||
return boto3.client('dynamodbstreams')
|
||||
else:
|
||||
# Even though we connect to the local installation, Boto3 still
|
||||
# requires us to specify dummy region and credential parameters,
|
||||
# otherwise the user is forced to properly configure ~/.aws even
|
||||
# for local runs.
|
||||
if request.config.getoption('url') != None:
|
||||
local_url = request.config.getoption('url')
|
||||
else:
|
||||
local_url = 'https://localhost:8043' if request.config.getoption('https') else 'http://localhost:8000'
|
||||
# Disable verifying in order to be able to use self-signed TLS certificates
|
||||
verify = not request.config.getoption('https')
|
||||
return boto3.client('dynamodbstreams', endpoint_url=local_url, verify=verify,
|
||||
region_name='us-east-1', aws_access_key_id='alternator', aws_secret_access_key='secret_pass',
|
||||
config=botocore.client.Config(retries={"max_attempts": 3}))
|
||||
region_name='us-east-1', aws_access_key_id='alternator', aws_secret_access_key='secret_pass')
|
||||
|
||||
# "test_table" fixture: Create and return a temporary table to be used in tests
|
||||
# that need a table to work on. The table is automatically deleted at the end.
|
||||
@@ -144,15 +115,6 @@ def test_table_s(dynamodb):
|
||||
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' } ])
|
||||
yield table
|
||||
table.delete()
|
||||
# test_table_s_2 has exactly the same schema as test_table_s, and is useful
|
||||
# for tests which need two different tables with the same schema.
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_s_2(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, ],
|
||||
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' } ])
|
||||
yield table
|
||||
table.delete()
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_b(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
@@ -174,13 +136,6 @@ def test_table_sn(dynamodb):
|
||||
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' }, { 'AttributeName': 'c', 'AttributeType': 'N' } ])
|
||||
yield table
|
||||
table.delete()
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_ss(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, { 'AttributeName': 'c', 'KeyType': 'RANGE' } ],
|
||||
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' }, { 'AttributeName': 'c', 'AttributeType': 'S' } ])
|
||||
yield table
|
||||
table.delete()
|
||||
|
||||
# "filled_test_table" fixture: Create a temporary table to be used in tests
|
||||
# that involve reading data - GetItem, Scan, etc. The table is filled with
|
||||
@@ -222,11 +177,3 @@ def filled_test_table(dynamodb):
|
||||
|
||||
yield table, items
|
||||
table.delete()
|
||||
|
||||
# The "scylla_only" fixture can be used by tests for Scylla-only features,
|
||||
# which do not exist on AWS DynamoDB. A test using this fixture will be
|
||||
# skipped if running with "--aws".
|
||||
@pytest.fixture(scope="session")
|
||||
def scylla_only(dynamodb):
|
||||
if dynamodb.meta.client._endpoint.host.endswith('.amazonaws.com'):
|
||||
pytest.skip('Scylla-only feature not supported by AWS')
|
||||
@@ -59,18 +59,6 @@ def test_expired_signature(dynamodb, test_table):
|
||||
assert not response.ok
|
||||
assert "InvalidSignatureException" in response.text and "Signature expired" in response.text
|
||||
|
||||
# A test verifying that missing Authorization header is handled properly
|
||||
def test_no_authorization_header(dynamodb, test_table):
|
||||
url = dynamodb.meta.client._endpoint.host
|
||||
print(url)
|
||||
headers = {'Content-Type': 'application/x-amz-json-1.0',
|
||||
'X-Amz-Date': '20170101T010101Z',
|
||||
'X-Amz-Target': 'DynamoDB_20120810.DescribeEndpoints',
|
||||
}
|
||||
response = requests.post(url, headers=headers, verify=False)
|
||||
assert not response.ok
|
||||
assert "InvalidSignatureException" in response.text and "Authorization header" in response.text
|
||||
|
||||
# A test ensuring that signatures that exceed current time too much are not accepted.
|
||||
# Watch out - this test is valid only for around next 1000 years, it needs to be updated later.
|
||||
def test_signature_too_futuristic(dynamodb, test_table):
|
||||
@@ -20,7 +20,6 @@
|
||||
# so they are actually tested by other tests as well.
|
||||
|
||||
import pytest
|
||||
import random
|
||||
from botocore.exceptions import ClientError
|
||||
from util import random_string, full_scan, full_query, multiset
|
||||
|
||||
@@ -45,19 +44,6 @@ def test_basic_batch_write_item(test_table):
|
||||
assert item['attribute'] == str(i)
|
||||
assert item['another'] == 'xyz'
|
||||
|
||||
# Try a batch which includes both multiple writes to the same partition
|
||||
# and several partitions. The LWT code collects multiple mutations to the
|
||||
# same partition together, and we want to test that this worked correctly.
|
||||
def test_batch_write_item_mixed(test_table):
|
||||
partitions = [random_string() for i in range(4)]
|
||||
items = [{'p': p, 'c': str(i)} for p in partitions for i in range(4)]
|
||||
with test_table.batch_writer() as batch:
|
||||
# Reorder items randomly, just for the heck of it
|
||||
for item in random.sample(items, len(items)):
|
||||
batch.put_item(item)
|
||||
for item in items:
|
||||
assert test_table.get_item(Key={'p': item['p'], 'c': item['c']}, ConsistentRead=True)['Item'] == item
|
||||
|
||||
# Test batch write to a table with only a hash key
|
||||
def test_batch_write_hash_only(test_table_s):
|
||||
items = [{'p': random_string(), 'val': random_string()} for i in range(10)]
|
||||
@@ -152,20 +138,6 @@ def test_batch_write_duplicate_write_and_delete(test_table_s, test_table):
|
||||
batch.put_item({'p': p, 'c': other})
|
||||
batch.put_item({'p': other, 'c': c})
|
||||
|
||||
# The BatchWriteIem API allows writing to more than one table in the same
|
||||
# batch. This test verifies that the duplicate-key checking doesn't mistake
|
||||
# updates to the same key in different tables to be duplicates.
|
||||
def test_batch_write_nonduplicate_multiple_tables(test_table_s, test_table_s_2):
|
||||
p = random_string()
|
||||
# The batch_writer() function used in previous tests can't write to more
|
||||
# than one table. So we use the lower level interface boto3 gives us.
|
||||
reply = test_table_s.meta.client.batch_write_item(RequestItems = {
|
||||
test_table_s.name: [{'PutRequest': {'Item': {'p': p, 'a': 'hi'}}}],
|
||||
test_table_s_2.name: [{'PutRequest': {'Item': {'p': p, 'b': 'hello'}}}]
|
||||
})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 'hi'}
|
||||
assert test_table_s_2.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 'hello'}
|
||||
|
||||
# Test that BatchWriteItem's PutRequest completely replaces an existing item.
|
||||
# It shouldn't merge it with a previously existing value. See also the same
|
||||
# test for PutItem - test_put_item_replace().
|
||||
@@ -210,32 +182,6 @@ def test_batch_write_invalid_operation(test_table_s):
|
||||
for p in [p1, p2]:
|
||||
assert not 'item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
|
||||
# In test_item.py we have a bunch of test_empty_* tests on different ways to
|
||||
# create an empty item (which in Scylla requires the special CQL row marker
|
||||
# to be supported correctly). BatchWriteItems provides yet another way of
|
||||
# creating items, so check the empty case here too:
|
||||
def test_empty_batch_write(test_table):
|
||||
p = random_string()
|
||||
c = random_string()
|
||||
with test_table.batch_writer() as batch:
|
||||
batch.put_item({'p': p, 'c': c})
|
||||
assert test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item'] == {'p': p, 'c': c}
|
||||
|
||||
# Test that BatchWriteItems allows writing to multiple tables in one operation
|
||||
def test_batch_write_multiple_tables(test_table_s, test_table):
|
||||
p1 = random_string()
|
||||
c1 = random_string()
|
||||
p2 = random_string()
|
||||
# We use the low-level batch_write_item API for lack of a more convenient
|
||||
# API (the batch_writer() API can only write to one table). At least it
|
||||
# spares us the need to encode the key's types...
|
||||
reply = test_table.meta.client.batch_write_item(RequestItems = {
|
||||
test_table.name: [{'PutRequest': {'Item': {'p': p1, 'c': c1, 'a': 'hi'}}}],
|
||||
test_table_s.name: [{'PutRequest': {'Item': {'p': p2, 'b': 'hello'}}}]
|
||||
})
|
||||
assert test_table.get_item(Key={'p': p1, 'c': c1}, ConsistentRead=True)['Item'] == {'p': p1, 'c': c1, 'a': 'hi'}
|
||||
assert test_table_s.get_item(Key={'p': p2}, ConsistentRead=True)['Item'] == {'p': p2, 'b': 'hello'}
|
||||
|
||||
# Basic test for BatchGetItem, reading several entire items.
|
||||
# Schema has both hash and sort keys.
|
||||
def test_batch_get_item(test_table):
|
||||
@@ -305,16 +251,3 @@ def test_batch_get_item_projection_expression(test_table):
|
||||
got_items = reply['Responses'][test_table.name]
|
||||
expected_items = [{k: item[k] for k in wanted if k in item} for item in items]
|
||||
assert multiset(got_items) == multiset(expected_items)
|
||||
|
||||
# Test that we return the required UnprocessedKeys/UnprocessedItems parameters
|
||||
def test_batch_unprocessed(test_table_s):
|
||||
p = random_string()
|
||||
write_reply = test_table_s.meta.client.batch_write_item(RequestItems = {
|
||||
test_table_s.name: [{'PutRequest': {'Item': {'p': p, 'a': 'hi'}}}],
|
||||
})
|
||||
assert 'UnprocessedItems' in write_reply and write_reply['UnprocessedItems'] == dict()
|
||||
|
||||
read_reply = test_table_s.meta.client.batch_get_item(RequestItems = {
|
||||
test_table_s.name: {'Keys': [{'p': p}], 'ProjectionExpression': 'p, a', 'ConsistentRead': True}
|
||||
})
|
||||
assert 'UnprocessedKeys' in read_reply and read_reply['UnprocessedKeys'] == dict()
|
||||
@@ -22,36 +22,9 @@
|
||||
# test_condition_expression.py. Many of the tests there are very similar to
|
||||
# the ones included here.
|
||||
|
||||
# NOTE: In this file, we use the b'xyz' syntax to represent DynamoDB's binary
|
||||
# values. This syntax works as expected only in Python3. In Python2 it
|
||||
# appears to work, but the "b" is actually ignored and the result is a normal
|
||||
# string 'xyz'. That means that we end up testing the string type instead of
|
||||
# the binary type as intended. So this test can run on Python2 but doesn't
|
||||
# cover testing binary types. The test should be run in Python3 to ensure full
|
||||
# coverage.
|
||||
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
from util import random_string
|
||||
from sys import version_info
|
||||
|
||||
# A helper function for changing write isolation policies
|
||||
def set_write_isolation(table, isolation):
|
||||
got = table.meta.client.describe_table(TableName=table.name)['Table']
|
||||
arn = got['TableArn']
|
||||
tags = [
|
||||
{
|
||||
'Key': 'system:write_isolation',
|
||||
'Value': isolation
|
||||
}
|
||||
]
|
||||
table.meta.client.tag_resource(ResourceArn=arn, Tags=tags)
|
||||
|
||||
# A helper function to clear previous isolation tags
|
||||
def clear_write_isolation(table):
|
||||
got = table.meta.client.describe_table(TableName=table.name)['Table']
|
||||
arn = got['TableArn']
|
||||
table.meta.client.untag_resource(ResourceArn=arn, TagKeys=['system:write_isolation'])
|
||||
|
||||
# Most of the tests in this file check that the ConditionExpression
|
||||
# parameter works for the UpdateItem operation. It should also work the
|
||||
@@ -88,6 +61,7 @@ def test_condition_expression_attribute_updates(test_table_s):
|
||||
# attribute from the request, and the case of comparing two different
|
||||
# attributes of the same item (the latter case wasn't possible to express
|
||||
# with Expected, and becomes possible with ConditionExpression).
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_eq_success(test_table_s):
|
||||
p = random_string()
|
||||
values = (1, "hello", True, b'xyz', None, ['hello', 42], {'hello': 'world'}, set(['hello', 'world']), set([1, 2, 3]), set([b'xyz', b'hi']))
|
||||
@@ -111,6 +85,7 @@ def test_update_condition_eq_success(test_table_s):
|
||||
|
||||
# Comparing values of *different* types should always fail. Check all the
|
||||
# combination of different types.
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_eq_different(test_table_s):
|
||||
p = random_string()
|
||||
values = (1, "hello", True, b'xyz', None, ['hello', 42], {'hello': 'world'}, set(['hello', 'world']), set([1, 2, 3]), set([b'xyz', b'hi']))
|
||||
@@ -137,6 +112,7 @@ def test_update_condition_eq_different(test_table_s):
|
||||
ExpressionAttributeValues={':val1': val1, ':val2': val2})
|
||||
|
||||
# Also check an actual case of same time, but inequality.
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_eq_unequal(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
@@ -150,6 +126,7 @@ def test_update_condition_eq_unequal(test_table_s):
|
||||
# Check that set equality is checked correctly. Unlike string equality (for
|
||||
# example), it cannot be done with just naive string comparison of the JSON
|
||||
# representation, and we need to allow for any order. (see issue #5021)
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_eq_set(test_table_s):
|
||||
p = random_string()
|
||||
# Because boto3 sorts the set values we give it, in order to generate a
|
||||
@@ -169,6 +146,7 @@ def test_update_condition_eq_set(test_table_s):
|
||||
assert 'b' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']
|
||||
|
||||
# Test for ConditionExpression with operator "<>" (non-equality),
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_ne(test_table_s):
|
||||
p = random_string()
|
||||
# We only check here one type of attributes (numbers), assuming that the
|
||||
@@ -209,6 +187,7 @@ def test_update_condition_ne(test_table_s):
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 3
|
||||
|
||||
# Test for ConditionExpression with operator "<"
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_lt(test_table_s):
|
||||
p = random_string()
|
||||
# The < operator should work for string, number and binary types
|
||||
@@ -281,6 +260,7 @@ def test_update_condition_lt(test_table_s):
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 4
|
||||
|
||||
# Test for ConditionExpression with operator "<="
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_le(test_table_s):
|
||||
p = random_string()
|
||||
# The <= operator should work for string, number and binary types
|
||||
@@ -344,6 +324,7 @@ def test_update_condition_le(test_table_s):
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 7
|
||||
|
||||
# Test for ConditionExpression with operator ">"
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_gt(test_table_s):
|
||||
p = random_string()
|
||||
# The > operator should work for string, number and binary types
|
||||
@@ -407,6 +388,7 @@ def test_update_condition_gt(test_table_s):
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 4
|
||||
|
||||
# Test for ConditionExpression with operator ">="
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_ge(test_table_s):
|
||||
p = random_string()
|
||||
# The >= operator should work for string, number and binary types
|
||||
@@ -472,6 +454,7 @@ def test_update_condition_ge(test_table_s):
|
||||
# Test for ConditionExpression with ternary operator "BETWEEN" (checking
|
||||
# if a value is between two others, equality included). The keywords
|
||||
# "BETWEEN" and "AND" are case insensitive.
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_between(test_table_s):
|
||||
p = random_string()
|
||||
# The BETWEEN operator should work for string, number and binary types
|
||||
@@ -553,6 +536,7 @@ def test_update_condition_between(test_table_s):
|
||||
# Test for ConditionExpression with multi-operand operator "IN", checking
|
||||
# whether a value is equal to one of possibly many values (up to 100 should
|
||||
# be supported, according to the DynamoDB documentation).
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_in(test_table_s):
|
||||
p = random_string()
|
||||
|
||||
@@ -599,12 +583,6 @@ def test_update_condition_in(test_table_s):
|
||||
ConditionExpression='a IN (:x, :y)',
|
||||
ExpressionAttributeValues={':val': 1, ':x': 'dog', ':y': 174})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 1
|
||||
# IN with zero arguments results in a syntax error, not a failed condition
|
||||
with pytest.raises(ClientError, match='ValidationException.*yntax error'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET c = :val37',
|
||||
ConditionExpression='a IN ()',
|
||||
ExpressionAttributeValues=values)
|
||||
|
||||
# Beyond the above operators, there are also test functions supported -
|
||||
# attribute_exists, attribute_not_exists, attribute_type, begins_with,
|
||||
@@ -612,6 +590,7 @@ def test_update_condition_in(test_table_s):
|
||||
# These functions are listed and described in
|
||||
# https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html
|
||||
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_attribute_exists(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
@@ -626,34 +605,8 @@ def test_update_condition_attribute_exists(test_table_s):
|
||||
UpdateExpression='SET c = :val',
|
||||
ConditionExpression='attribute_exists (z)',
|
||||
ExpressionAttributeValues={':val': 3})
|
||||
# Somewhat artificially, attribute_exists() requires that its parameter
|
||||
# be a path - it cannot be a different sort of value.
|
||||
with pytest.raises(ClientError, match='ValidationException.*path'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET c = :val',
|
||||
ConditionExpression='attribute_exists (:val)',
|
||||
ExpressionAttributeValues={':val': 3})
|
||||
|
||||
# Primitive conditions usually look like an operator between two (<, <=,
|
||||
# etc.), three (BETWEEN) or more (IN) values. Can just a single value be
|
||||
# a condition? The special case of a single function call *can* be - we saw
|
||||
# an example attribute_exists(z) in the previous test. However that only
|
||||
# function calls are supported in this context - not general values (i.e.,
|
||||
# attribute or value references).
|
||||
# While DynamoDB does not accept a non-function-call value as a condition
|
||||
# (it results with with a syntax error), in Alternator currently, for
|
||||
# simplicity of the parser, this case is parsed correctly and only fails
|
||||
# later when the calculated value ends up to not be a boolean.
|
||||
def test_update_condition_single_value_attribute(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET c = :val',
|
||||
ConditionExpression='a',
|
||||
ExpressionAttributeValues={':val': 1})
|
||||
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_attribute_not_exists(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
@@ -669,6 +622,7 @@ def test_update_condition_attribute_not_exists(test_table_s):
|
||||
ConditionExpression='attribute_not_exists (a)',
|
||||
ExpressionAttributeValues={':val': 3})
|
||||
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_attribute_type(test_table_s):
|
||||
p = random_string()
|
||||
type_values = [
|
||||
@@ -686,10 +640,6 @@ def test_update_condition_attribute_type(test_table_s):
|
||||
test_table_s.update_item(Key={'p': p}, AttributeUpdates=updates)
|
||||
for i in range(len(type_values)):
|
||||
expected_type = type_values[i][0]
|
||||
# As explained in a comment in the top of the file, the binary types
|
||||
# cannot be tested with Python 2
|
||||
if expected_type in ('B', 'BS') and version_info[0] == 2:
|
||||
continue
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET c = :val',
|
||||
ConditionExpression='attribute_type (a{}, :type)'.format(i),
|
||||
@@ -701,43 +651,25 @@ def test_update_condition_attribute_type(test_table_s):
|
||||
UpdateExpression='SET c = :val',
|
||||
ConditionExpression='attribute_type (a{}, :type)'.format(i),
|
||||
ExpressionAttributeValues={':val': i, ':type': wrong_type})
|
||||
# The DynamoDB documentation suggests that attribute_type()'s first
|
||||
# parameter must be a path (as we saw above, this is indeed the case for
|
||||
# attribute_exists()). But in fact, attribute_type() does work fine also
|
||||
# for an expression attribute.
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET c = :val',
|
||||
ConditionExpression='attribute_type (:val, :type)',
|
||||
ExpressionAttributeValues={':val': 0, ':type': 'N'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 0
|
||||
|
||||
# The DynamoDB documentation explicitly states that the second argument
|
||||
# of the attribute_type function - the type to compare to - *must* be an
|
||||
# expression attribute (:name) - it cannot be an item attribute.
|
||||
# I don't know why this was important to forbid, but this test confirms that
|
||||
# DynamoDB does forbid it.
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_attribute_type_second_arg(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
||||
'b': {'Value': 'N', 'Action': 'PUT'}})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
with pytest.raises(ClientError, match='ValidationException.*Incorrect'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET c = :val',
|
||||
ConditionExpression='attribute_type (a, b)',
|
||||
ExpressionAttributeValues={':val': 1})
|
||||
|
||||
# If the attribute_type() parameter is not one of the known types
|
||||
# (N,NS,BS,L,SS,NULL,B,BOOL,S,M), an error is generated. We should
|
||||
# not get a failed condition.
|
||||
def test_update_condition_attribute_type_unknown(test_table_s):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException.*DOG'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET c = :val',
|
||||
ConditionExpression='attribute_type (a, :type)',
|
||||
ExpressionAttributeValues={':val': 1, ':type': 'DOG'})
|
||||
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_begins_with(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
@@ -766,11 +698,6 @@ def test_update_condition_begins_with(test_table_s):
|
||||
UpdateExpression='SET c = :val',
|
||||
ConditionExpression='begins_with(a, :arg)',
|
||||
ExpressionAttributeValues={':val': 3, ':arg': 2})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET c = :val',
|
||||
ConditionExpression='begins_with(c, :arg)',
|
||||
ExpressionAttributeValues={':val': 3, ':arg': 2})
|
||||
# However, that extra type check is only done on values inside the
|
||||
# expression. It isn't done on values from an item attributes - in that
|
||||
# case we got a normal failed condition.
|
||||
@@ -784,34 +711,19 @@ def test_update_condition_begins_with(test_table_s):
|
||||
UpdateExpression='SET c = :val',
|
||||
ConditionExpression='begins_with(c, a)',
|
||||
ExpressionAttributeValues={':val': 3})
|
||||
# Although the DynamoDB documentation suggests that begins_with()
|
||||
# can only take a path as the first parameter and a constant as
|
||||
# the second, this isn't actually true - begins_with() works
|
||||
# as expected also to compare two attributes, or in reverse order:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET c = :val',
|
||||
ConditionExpression='begins_with(:str, a)',
|
||||
ExpressionAttributeValues={':val': 'he', ':str': 'hellohi'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 'he'
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET c = :val',
|
||||
ConditionExpression='begins_with(a, c)',
|
||||
ExpressionAttributeValues={':val': 5})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 5
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_contains(test_table_s):
|
||||
p = random_string()
|
||||
# contains() can be used for two unrelated things: check substring (in
|
||||
# string or binary) and membership (in set or a list). The DynamoDB
|
||||
# documentation only mention string and set (not binary or list) but
|
||||
# documentation only bention string and set (not binary or list) but
|
||||
# the fact is that binary and list are also support.
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 'hello', 'Action': 'PUT'},
|
||||
'b': {'Value': set([2, 4, 7]), 'Action': 'PUT'},
|
||||
'c': {'Value': [2, 4, 7], 'Action': 'PUT'},
|
||||
'd': {'Value': b'hi there', 'Action': 'PUT'},
|
||||
'e': {'Value': ['hi', set([1,2]), [3, 4]], 'Action': 'PUT'}})
|
||||
'd': {'Value': b'hi there', 'Action': 'PUT'}})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET z = :val',
|
||||
ConditionExpression='contains(a, :arg)',
|
||||
@@ -840,37 +752,11 @@ def test_update_condition_contains(test_table_s):
|
||||
UpdateExpression='SET z = :val',
|
||||
ConditionExpression='contains(d, :arg)',
|
||||
ExpressionAttributeValues={':val': 4, ':arg': b'dog'})
|
||||
# Moreover, the second parameter to contains() may be *any* type, and
|
||||
# contains checks if perhaps the first parameter is a list or a set
|
||||
# containing that value!
|
||||
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET z = :val',
|
||||
ConditionExpression='contains(d, :arg)',
|
||||
ExpressionAttributeValues={':val': 4, ':arg': set([1, 2])})
|
||||
# While both operands of contains may be item attributes, strangely
|
||||
# it is explicitly forbidden to have the same attribute as both and
|
||||
# trying to do so results in a ValidationException.
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET z = :val',
|
||||
ConditionExpression='contains(e, :arg)',
|
||||
ExpressionAttributeValues={':val': 5, ':arg': set([1, 2])})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 5
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET z = :val',
|
||||
ConditionExpression='contains(e, :arg)',
|
||||
ExpressionAttributeValues={':val': 6, ':arg': [3, 4]})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 6
|
||||
|
||||
|
||||
# While both operands of contains() may be item attributes, strangely
|
||||
# it is explicitly forbidden to have the same attribute as both and
|
||||
# trying to do so results in a ValidationException. I don't know why it's
|
||||
# important to make this query fail, when it could have just worked...
|
||||
# TODO: Is this limitation only for contains() or other functions as well?
|
||||
@pytest.mark.xfail(reason="extra check for same attribute not implemented yet")
|
||||
def test_update_condition_contains_same_attribute(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a1': {'Value': 'hello', 'Action': 'PUT'},
|
||||
'a': {'Value': 'hello', 'Action': 'PUT'}})
|
||||
AttributeUpdates={'a1': {'Value': 'hello', 'Action': 'PUT'}})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET z = :val',
|
||||
ConditionExpression='contains(a, a1)',
|
||||
@@ -888,6 +774,7 @@ def test_update_condition_contains_same_attribute(test_table_s):
|
||||
# function whose return value needs to be further combined with another
|
||||
# operand using a comparison operation - and it isn't specified which is
|
||||
# supported.
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_size(test_table_s):
|
||||
p = random_string()
|
||||
# First verify what size() returns for various types. We use only the
|
||||
@@ -897,7 +784,7 @@ def test_update_condition_size(test_table_s):
|
||||
'b': {'Value': set([2, 4, 7]), 'Action': 'PUT'},
|
||||
'c': {'Value': [2, 'dog', 7], 'Action': 'PUT'},
|
||||
'd': {'Value': b'hi there', 'Action': 'PUT'},
|
||||
'e': {'Value': {'x': 2, 'y': {'m': 3, 'n': 4}}, 'Action': 'PUT'},
|
||||
'e': {'Value': {'x': 2, 'y': 3}, 'Action': 'PUT'},
|
||||
'f': {'Value': 5, 'Action': 'PUT'},
|
||||
'g': {'Value': True, 'Action': 'PUT'},
|
||||
'h': {'Value': None, 'Action': 'PUT'}})
|
||||
@@ -982,70 +869,6 @@ def test_update_condition_size(test_table_s):
|
||||
ConditionExpression='size(a)>=:arg',
|
||||
ExpressionAttributeValues={':val': 11, ':arg': 2})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 11
|
||||
# size() is only allowed one operand; More operands are allowed by the
|
||||
# parser, but later result in an error:
|
||||
with pytest.raises(ClientError, match='ValidationException.*2'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET z = :val',
|
||||
ConditionExpression='size(a, a)=:arg',
|
||||
ExpressionAttributeValues={':val': 1, ':arg': 5})
|
||||
|
||||
# The above test tested conditions involving size() in a comparison.
|
||||
# Trying to use just size(a) as a condition (as we use the rest of the
|
||||
# functions supported by ConditionExpression) does not work - DynamoDB
|
||||
# reports # that "The function is not allowed to be used this way in an
|
||||
# expression; function: size".
|
||||
def test_update_condition_size_alone(test_table_s):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET z = :val',
|
||||
ConditionExpression='size(a)',
|
||||
ExpressionAttributeValues={':val': 1})
|
||||
|
||||
# Similarly, while attribute_exists(a) works alone, it cannot be used in
|
||||
# a comparison, e.g., attribute_exists(a) < 1 also causes DynamoDB to
|
||||
# complain about "The function is not allowed to be used in this way in an
|
||||
# expression.".
|
||||
def test_update_condition_attribute_exists_in_comparison(test_table_s):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET z = :val',
|
||||
ConditionExpression='attribute_exists(a) < :val',
|
||||
ExpressionAttributeValues={':val': 1})
|
||||
|
||||
# In essense, the size() function tested in the previous test behaves
|
||||
# exactly like the functions of UpdateExpressions, i.e., it transforms a
|
||||
# value (attribute from the item or the query) into a new value, which
|
||||
# can than be operated (in our case, compared). In this test we check
|
||||
# that other functions supported by UpdateExpression - if_not_exists()
|
||||
# and list_append() - are not supported.
|
||||
def test_update_condition_other_funcs(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 'hello', 'Action': 'PUT'}})
|
||||
# dog() is an unknown function name:
|
||||
with pytest.raises(ClientError, match='ValidationException.*function'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET z = :val',
|
||||
ConditionExpression='dog(a)=:arg',
|
||||
ExpressionAttributeValues={':val': 1, ':arg': 5})
|
||||
# The functions if_not_exists() and list_append() are known functions
|
||||
# (they are supported in UpdateExpression) but not allowed in
|
||||
# ConditionExpression. This means we can have a single function for
|
||||
# evaluation a parsed::value, but it needs to know whether it is
|
||||
# called for a UpdateExpression or a ConditionExpression.
|
||||
with pytest.raises(ClientError, match='ValidationException.*not allowed'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET z = :val',
|
||||
ConditionExpression='if_not_exists(a, a)=:arg',
|
||||
ExpressionAttributeValues={':val': 1, ':arg': 5})
|
||||
with pytest.raises(ClientError, match='ValidationException.*not allowed'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET z = :val',
|
||||
ConditionExpression='list_append(a, a)=:arg',
|
||||
ExpressionAttributeValues={':val': 1, ':arg': 5})
|
||||
|
||||
# All the previous tests involved top-level attributes to be tested. But
|
||||
# ConditionExpressions also allows reading nested attributes, and we should
|
||||
@@ -1071,6 +894,7 @@ def test_update_condition_nested_attributes(test_table_s):
|
||||
# But the DynamoDB API also allows to refer to attributes using a #reference.
|
||||
# Among other things this allows using attribute names which are usually
|
||||
# reserved keywords in condition expressions.
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_attribute_reference(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
@@ -1100,6 +924,7 @@ def test_update_condition_nested_attribute_reference(test_table_s):
|
||||
# precedence involved, and should be tested (see the definitions in
|
||||
# https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html
|
||||
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_and(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
@@ -1124,6 +949,7 @@ def test_update_condition_and(test_table_s):
|
||||
ConditionExpression='a < b AND c < b',
|
||||
ExpressionAttributeValues={':val': 1})
|
||||
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_or(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
@@ -1153,6 +979,7 @@ def test_update_condition_or(test_table_s):
|
||||
ConditionExpression='b < a OR c < b',
|
||||
ExpressionAttributeValues={':val': 1})
|
||||
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_not(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
@@ -1175,13 +1002,8 @@ def test_update_condition_not(test_table_s):
|
||||
UpdateExpression='SET z = :val',
|
||||
ConditionExpression='NOT a < b',
|
||||
ExpressionAttributeValues={':val': 1})
|
||||
# NOT NOT NOT NOT also works (and does nothing) :-)
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET z = :val',
|
||||
ConditionExpression='NOT NOT NOT NOT a < b',
|
||||
ExpressionAttributeValues={':val': 3})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 3
|
||||
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_parentheses(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
@@ -1197,6 +1019,7 @@ def test_update_condition_parentheses(test_table_s):
|
||||
# There is operator precedence that allows a user to use less parentheses.
|
||||
# We need to implement it correctly:
|
||||
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_and_before_or(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
@@ -1209,6 +1032,7 @@ def test_update_condition_and_before_or(test_table_s):
|
||||
ExpressionAttributeValues={':val': 1})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 1
|
||||
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_not_before_and(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
@@ -1221,6 +1045,7 @@ def test_update_condition_not_before_and(test_table_s):
|
||||
ConditionExpression='NOT a < b AND c < b',
|
||||
ExpressionAttributeValues={':val': 1})
|
||||
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_between_before_and(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
@@ -1235,6 +1060,7 @@ def test_update_condition_between_before_and(test_table_s):
|
||||
|
||||
# An empty ConditionExpression is not allowed - resulting in a validation
|
||||
# error, not a failed condition:
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_empty(test_table_s):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
@@ -1250,6 +1076,7 @@ def test_update_condition_empty(test_table_s):
|
||||
# used to test the condition. So we just need one test for each operation,
|
||||
# to verify that this code actually gets called.
|
||||
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_delete_item_condition(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
@@ -1264,6 +1091,7 @@ def test_delete_item_condition(test_table_s):
|
||||
ExpressionAttributeValues={':oldval': 1})
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_put_item_condition(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
@@ -1276,222 +1104,3 @@ def test_put_item_condition(test_table_s):
|
||||
test_table_s.put_item(Item={'p': p, 'a': 3},
|
||||
ConditionExpression='a = :oldval',
|
||||
ExpressionAttributeValues={':oldval': 1})
|
||||
|
||||
# DynamoDB frowns upon unused entries in ExpressionAttributeValues and
|
||||
# ExpressionAttributeNames. Check that we do too (in all three operations),
|
||||
# although it's not terribly important that we be compatible with DynamoDB
|
||||
# here...
|
||||
# There's one delicate issue, though. Should we check for unused entries
|
||||
# during parsing, or during evaluation? The stage we check this changes
|
||||
# our behavior when the condition was supposed to fail. So we have two
|
||||
# separate tests here, one for failed condition and one for successful.
|
||||
def test_update_condition_unused_entries_failed(test_table_s):
|
||||
p = random_string()
|
||||
# unused val3:
|
||||
with pytest.raises(ClientError, match='ValidationException.*val3'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET #name1 = :val1',
|
||||
ConditionExpression='#name2 = :val2',
|
||||
ExpressionAttributeValues={':val1': 1, ':val2': 2, ':val3': 3},
|
||||
ExpressionAttributeNames={'#name1': 'a', '#name2': 'b'})
|
||||
with pytest.raises(ClientError, match='ValidationException.*val3'):
|
||||
test_table_s.delete_item(Key={'p': p},
|
||||
ConditionExpression='#name1 = :val1',
|
||||
ExpressionAttributeValues={':val1': 1, ':val3': 3},
|
||||
ExpressionAttributeNames={'#name1': 'a'})
|
||||
with pytest.raises(ClientError, match='ValidationException.*val3'):
|
||||
test_table_s.put_item(Item={'p': p, 'a': 3},
|
||||
ConditionExpression='#name1 = :val1',
|
||||
ExpressionAttributeValues={':val1': 1, ':val3': 3},
|
||||
ExpressionAttributeNames={'#name1': 'a'})
|
||||
# unused name3:
|
||||
with pytest.raises(ClientError, match='ValidationException.*name3'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET #name1 = :val1',
|
||||
ConditionExpression='#name2 = :val2',
|
||||
ExpressionAttributeValues={':val1': 1, ':val2': 2},
|
||||
ExpressionAttributeNames={'#name1': 'a', '#name2': 'b', '#name3': 'c'})
|
||||
with pytest.raises(ClientError, match='ValidationException.*name3'):
|
||||
test_table_s.delete_item(Key={'p': p},
|
||||
ConditionExpression='#name1 = :val1',
|
||||
ExpressionAttributeValues={':val1': 1},
|
||||
ExpressionAttributeNames={'#name1': 'a', '#name3': 'c'})
|
||||
with pytest.raises(ClientError, match='ValidationException.*name3'):
|
||||
test_table_s.put_item(Item={'p': p, 'a': 3},
|
||||
ConditionExpression='#name1 = :val1',
|
||||
ExpressionAttributeValues={':val1': 1},
|
||||
ExpressionAttributeNames={'#name1': 'a', '#name3': 'c'})
|
||||
def test_update_condition_unused_entries_succeeded(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': 2, 'Action': 'PUT'}})
|
||||
# unused val3:
|
||||
with pytest.raises(ClientError, match='ValidationException.*val3'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET #name1 = :val1',
|
||||
ConditionExpression='#name2 = :val2',
|
||||
ExpressionAttributeValues={':val1': 1, ':val2': 2, ':val3': 3},
|
||||
ExpressionAttributeNames={'#name1': 'a', '#name2': 'b'})
|
||||
with pytest.raises(ClientError, match='ValidationException.*val3'):
|
||||
test_table_s.delete_item(Key={'p': p},
|
||||
ConditionExpression='#name2 = :val2',
|
||||
ExpressionAttributeValues={':val2': 2, ':val3': 3},
|
||||
ExpressionAttributeNames={'#name2': 'b'})
|
||||
with pytest.raises(ClientError, match='ValidationException.*val3'):
|
||||
test_table_s.put_item(Item={'p': p, 'a': 3},
|
||||
ConditionExpression='#name2 = :val2',
|
||||
ExpressionAttributeValues={':val2': 2, ':val3': 3},
|
||||
ExpressionAttributeNames={'#name2': 'b'})
|
||||
# unused name3:
|
||||
with pytest.raises(ClientError, match='ValidationException.*name3'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET #name1 = :val1',
|
||||
ConditionExpression='#name2 = :val2',
|
||||
ExpressionAttributeValues={':val1': 1, ':val2': 2},
|
||||
ExpressionAttributeNames={'#name1': 'a', '#name2': 'b', '#name3': 'c'})
|
||||
with pytest.raises(ClientError, match='ValidationException.*name3'):
|
||||
test_table_s.delete_item(Key={'p': p},
|
||||
ConditionExpression='#name2 = :val2',
|
||||
ExpressionAttributeValues={':val2': 2},
|
||||
ExpressionAttributeNames={'#name2': 'b', '#name3': 'c'})
|
||||
with pytest.raises(ClientError, match='ValidationException.*name3'):
|
||||
test_table_s.put_item(Item={'p': p, 'a': 3},
|
||||
ConditionExpression='#name2 = :val2',
|
||||
ExpressionAttributeValues={':val2': 2},
|
||||
ExpressionAttributeNames={'#name2': 'b', '#name3': 'c'})
|
||||
|
||||
# Another reason why we must test for used references right after parsing
|
||||
# the expressions, NOT at evaluation time, is that in some cases evaluation
|
||||
# may short-circuit and not reach certain parts of the expression, and as
|
||||
# a result we may wrongly think some names were not used, and refuse a
|
||||
# perfectly good request. Such a bug (see issue #6572) can be fixed by
|
||||
# either by dropping short-circuit evaluation (i.e., evaluate all parts
|
||||
# of the expression even if the first OR succeeds), or by testing for
|
||||
# unused references before evaluating anything.
|
||||
def test_update_condition_unused_entries_short_circuit(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
||||
# If short-circuit evaluation is done for ConditionExpression, it will
|
||||
# not use #name2 or :val2. But we should't fail this request claiming
|
||||
# these references weren't used... They were used in the expression,
|
||||
# just not in the evaluation. This request *should* work.
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
ConditionExpression='#name1 = :val1 OR #name2 = :val2',
|
||||
UpdateExpression='SET #name1 = :val3',
|
||||
ExpressionAttributeValues={':val1': 1, ':val2': 2, ':val3': 3},
|
||||
ExpressionAttributeNames={'#name1': 'a', '#name2': 'b'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 3}
|
||||
|
||||
# Test a bunch of cases with permissive write isolation levels,
|
||||
# i.e. LWT_ALWAYS, LWT_RMW_ONLY and UNSAFE_RMW.
|
||||
# These test cases make sense only for alternator, so they're skipped
|
||||
# when run on AWS
|
||||
def test_condition_expression_with_permissive_write_isolation(scylla_only, dynamodb, test_table_s):
|
||||
def do_test_with_permissive_isolation_levels(test_case, table, *args):
|
||||
try:
|
||||
for isolation in ['a', 'o', 'u']:
|
||||
set_write_isolation(table, isolation)
|
||||
test_case(table, *args)
|
||||
finally:
|
||||
clear_write_isolation(table)
|
||||
for test_case in [test_update_condition_eq_success, test_update_condition_attribute_exists,
|
||||
test_delete_item_condition, test_put_item_condition, test_update_condition_attribute_reference]:
|
||||
do_test_with_permissive_isolation_levels(test_case, test_table_s)
|
||||
|
||||
# Test that the forbid_rmw isolation level prevents read-modify-write requests
|
||||
# from working. These test cases make sense only for alternator, so they're skipped
|
||||
# when run on AWS
|
||||
def test_condition_expression_with_forbidden_rmw(scylla_only, dynamodb, test_table_s):
|
||||
def do_test_with_forbidden_rmw(test_case, table, *args):
|
||||
try:
|
||||
set_write_isolation(table, 'f')
|
||||
test_case(table, *args)
|
||||
assert False, "Expected an exception when running {}".format(test_case.__name__)
|
||||
except ClientError:
|
||||
pass
|
||||
finally:
|
||||
clear_write_isolation(table)
|
||||
for test_case in [test_update_condition_eq_success, test_update_condition_attribute_exists,
|
||||
test_put_item_condition, test_update_condition_attribute_reference]:
|
||||
do_test_with_forbidden_rmw(test_case, test_table_s)
|
||||
# Ensure that regular writes (without rmw) work just fine
|
||||
s = random_string()
|
||||
test_table_s.put_item(Item={'p': s, 'regular': 'write'})
|
||||
assert test_table_s.get_item(Key={'p': s}, ConsistentRead=True)['Item'] == {'p': s, 'regular': 'write'}
|
||||
test_table_s.update_item(Key={'p': s}, AttributeUpdates={'write': {'Value': 'regular', 'Action': 'PUT'}})
|
||||
assert test_table_s.get_item(Key={'p': s}, ConsistentRead=True)['Item'] == {'p': s, 'regular': 'write', 'write': 'regular'}
|
||||
|
||||
# Reproducer for issue #6573: binary strings should be ordered as unsigned
|
||||
# bytes, i.e., byte 128 comes after 127, not before as with signed bytes.
|
||||
# Test the five ordering operators: <, <=, >, >=, between
|
||||
def test_condition_expression_unsigned_bytes(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'b': bytearray([127])})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET z = :newval',
|
||||
ConditionExpression='b < :oldval',
|
||||
ExpressionAttributeValues={':newval': 1, ':oldval': bytearray([128])})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 1
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET z = :newval',
|
||||
ConditionExpression='b <= :oldval',
|
||||
ExpressionAttributeValues={':newval': 2, ':oldval': bytearray([128])})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 2
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET z = :newval',
|
||||
ConditionExpression='b between :oldval1 and :oldval2',
|
||||
ExpressionAttributeValues={':newval': 3, ':oldval1': bytearray([126]), ':oldval2': bytearray([128])})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 3
|
||||
|
||||
test_table_s.put_item(Item={'p': p, 'b': bytearray([128])})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET z = :newval',
|
||||
ConditionExpression='b > :oldval',
|
||||
ExpressionAttributeValues={':newval': 4, ':oldval': bytearray([127])})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 4
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET z = :newval',
|
||||
ConditionExpression='b >= :oldval',
|
||||
ExpressionAttributeValues={':newval': 5, ':oldval': bytearray([127])})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 5
|
||||
|
||||
# In all other tests above, we use ConditionExpression to check a condition
|
||||
# on one the non-key attributes. In this test we confirm that a condition may
|
||||
# also be on a key attribute. We demonstrate this through a useful DynamoDB
|
||||
# idiom for creating an item unless an item already exists with the same key,
|
||||
# by using a "<>" (not equal) condition.
|
||||
def test_update_item_condition_key_ne(test_table_s):
|
||||
p = random_string()
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
# Create an empty item with key p, but only an item with p exists yet.
|
||||
# Note how when the item does not exist, the <> (not equal) test succeeds
|
||||
# (we already tested that in test_update_condition_ne())
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
ConditionExpression='p <> :p',
|
||||
ExpressionAttributeValues={':p': p})
|
||||
assert 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
# If we do the same again, the item does exist, and the <> condition will
|
||||
# fail.
|
||||
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
ConditionExpression='p <> :p',
|
||||
ExpressionAttributeValues={':p': p})
|
||||
|
||||
# Another example of a condition on the key, again an idiom for creating an
|
||||
# item if no item already has that key. This time, using the
|
||||
# attribute_not_exists() instead of the <> (not equal) operator in the test
|
||||
# above.
|
||||
def test_update_item_condition_key_attribute_not_exists(test_table_s):
|
||||
p = random_string()
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
# Create an empty item with key p, but only an item with p exists yet.
|
||||
# Note how when the item does not exist, attribute_not_exists() succeeds
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
ConditionExpression='attribute_not_exists(p)')
|
||||
assert 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
# If we do the same again, the item does exist, and the
|
||||
# attribute_not_exists() condition will fail.
|
||||
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
ConditionExpression='attribute_not_exists(p)')
|
||||
@@ -141,16 +141,19 @@ def test_describe_table_stream_specification(test_table):
|
||||
# includes which zone it is on, which account, and of course the table's
|
||||
# name. The ARN format is described in
|
||||
# https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#genref-arns
|
||||
@pytest.mark.xfail(reason="DescribeTable does not return ARN")
|
||||
def test_describe_table_arn(test_table):
|
||||
got = test_table.meta.client.describe_table(TableName=test_table.name)['Table']
|
||||
assert 'TableArn' in got and got['TableArn'].startswith('arn:')
|
||||
|
||||
# Test that the table has a TableId.
|
||||
# DynamoDB documentation states that this id must look like a UUID.
|
||||
# TODO: Figure out what is this TableId supposed to be, it is just a
|
||||
# unique id that is created with the table and never changes? Or anything
|
||||
# else?
|
||||
@pytest.mark.xfail(reason="DescribeTable does not return TableId")
|
||||
def test_describe_table_id(test_table):
|
||||
got = test_table.meta.client.describe_table(TableName=test_table.name)['Table']
|
||||
assert 'TableId' in got
|
||||
assert re.fullmatch('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', got['TableId'])
|
||||
|
||||
# DescribeTable error path: trying to describe a non-existent table should
|
||||
# result in a ResourceNotFoundException.
|
||||
@@ -522,15 +522,6 @@ def test_update_expected_1_null(test_table_s):
|
||||
Expected={'a': {'ComparisonOperator': 'NULL', 'AttributeValueList': [2]}}
|
||||
)
|
||||
|
||||
# When ComparisonOperator = "NULL", AttributeValueList should be empty if it
|
||||
# exists, but as this test verifies, it may also be missing completely.
|
||||
def test_update_expected_1_null_missing_list(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 2, 'Action': 'PUT'}},
|
||||
Expected={'a': {'ComparisonOperator': 'NULL'}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == 2
|
||||
|
||||
# Tests for Expected with ComparisonOperator = "CONTAINS":
|
||||
def test_update_expected_1_contains(test_table_s):
|
||||
# true cases. CONTAINS can be used for two unrelated things: check substrings
|
||||
@@ -607,10 +598,6 @@ def test_update_expected_1_contains(test_table_s):
|
||||
AttributeUpdates={'z': {'Value': 17, 'Action': 'PUT'}},
|
||||
Expected={'a': {'ComparisonOperator': 'CONTAINS', 'AttributeValueList': []}}
|
||||
)
|
||||
# Strangely, while ConditionExpression's contains() allows the argument
|
||||
# to be of any type and checks if the attribute is perhaps a list
|
||||
# containing that item, Expected's "CONTAINS" is more limited, and
|
||||
# refuses a list as the argument (to be searched in a list of lists)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'z': {'Value': 17, 'Action': 'PUT'}},
|
||||
@@ -697,10 +684,6 @@ def test_update_expected_1_not_contains(test_table_s):
|
||||
AttributeUpdates={'z': {'Value': 17, 'Action': 'PUT'}},
|
||||
Expected={'a': {'ComparisonOperator': 'NOT_CONTAINS', 'AttributeValueList': []}}
|
||||
)
|
||||
# Strangely, while ConditionExpression's contains() allows the argument
|
||||
# to be of any type and checks if the attribute is perhaps a list
|
||||
# containing that item, Expected's "CONTAINS" is more limited, and
|
||||
# refuses a list as the argument (to be searched in a list of lists)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'z': {'Value': 17, 'Action': 'PUT'}},
|
||||
@@ -1069,19 +1052,6 @@ def test_update_expected_empty(test_table_s):
|
||||
AttributeUpdates={'z': {'Value': 4, 'Action': 'PUT'}},
|
||||
Expected={}, ConditionalOperator='AND')
|
||||
|
||||
# Specifying ConditionalOperator is forbidden if the "Expected" Attribute
|
||||
# is missing:
|
||||
def test_conditional_operator_expected_missing(test_table_s):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException.*ConditionalOperator'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'z': {'Value': 4, 'Action': 'PUT'}},
|
||||
ConditionalOperator='OR')
|
||||
with pytest.raises(ClientError, match='ValidationException.*ConditionalOperator'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'z': {'Value': 4, 'Action': 'PUT'}},
|
||||
ConditionalOperator='AND')
|
||||
|
||||
# All of the above tests tested "Expected" with the UpdateItem operation.
|
||||
# We now want to test that it works also with the PutItem and DeleteItems
|
||||
# operations. We don't need to check again all the different sub-cases tested
|
||||
@@ -1107,42 +1077,3 @@ def test_put_item_expected(test_table_s):
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 2}
|
||||
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
||||
test_table_s.put_item(Item={'p': p, 'a': 3}, Expected={'a': {'Value': 1}})
|
||||
|
||||
# Reproducer for issue #6573: binary strings should be ordered as unsigned
|
||||
# bytes, i.e., byte 128 comes after 127, not before as with signed bytes.
|
||||
# Test the five ordering operators: LT, LE, GT, GE, BETWEEN
|
||||
def test_update_expected_unsigned_bytes(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'b': bytearray([127])})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'z': {'Value': 1, 'Action': 'PUT'}},
|
||||
Expected={'b': {'ComparisonOperator': 'LT',
|
||||
'AttributeValueList': [bytearray([128])]}}
|
||||
)
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 1
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'z': {'Value': 2, 'Action': 'PUT'}},
|
||||
Expected={'b': {'ComparisonOperator': 'LE',
|
||||
'AttributeValueList': [bytearray([128])]}}
|
||||
)
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 2
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'z': {'Value': 3, 'Action': 'PUT'}},
|
||||
Expected={'b': {'ComparisonOperator': 'BETWEEN',
|
||||
'AttributeValueList': [bytearray([126]), bytearray([128])]}}
|
||||
)
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 3
|
||||
|
||||
test_table_s.put_item(Item={'p': p, 'b': bytearray([128])})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'z': {'Value': 4, 'Action': 'PUT'}},
|
||||
Expected={'b': {'ComparisonOperator': 'GT',
|
||||
'AttributeValueList': [bytearray([127])]}}
|
||||
)
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 4
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'z': {'Value': 5, 'Action': 'PUT'}},
|
||||
Expected={'b': {'ComparisonOperator': 'GE',
|
||||
'AttributeValueList': [bytearray([127])]}}
|
||||
)
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 5
|
||||
@@ -37,25 +37,22 @@ from util import create_test_table, random_string, full_scan, full_query, multis
|
||||
# retry.
|
||||
def assert_index_query(table, index_name, expected_items, **kwargs):
|
||||
for i in range(3):
|
||||
if multiset(expected_items) == multiset(full_query(table, IndexName=index_name, ConsistentRead=False, **kwargs)):
|
||||
if multiset(expected_items) == multiset(full_query(table, IndexName=index_name, **kwargs)):
|
||||
return
|
||||
print('assert_index_query retrying')
|
||||
time.sleep(1)
|
||||
assert multiset(expected_items) == multiset(full_query(table, IndexName=index_name, ConsistentRead=False, **kwargs))
|
||||
assert multiset(expected_items) == multiset(full_query(table, IndexName=index_name, **kwargs))
|
||||
|
||||
def assert_index_scan(table, index_name, expected_items, **kwargs):
|
||||
for i in range(3):
|
||||
if multiset(expected_items) == multiset(full_scan(table, IndexName=index_name, ConsistentRead=False, **kwargs)):
|
||||
if multiset(expected_items) == multiset(full_scan(table, IndexName=index_name, **kwargs)):
|
||||
return
|
||||
print('assert_index_scan retrying')
|
||||
time.sleep(1)
|
||||
assert multiset(expected_items) == multiset(full_scan(table, IndexName=index_name, ConsistentRead=False, **kwargs))
|
||||
assert multiset(expected_items) == multiset(full_scan(table, IndexName=index_name, **kwargs))
|
||||
|
||||
# Although quite silly, it is actually allowed to create an index which is
|
||||
# identical to the base table.
|
||||
# The following test does not work for KA/LA tables due to #6157,
|
||||
# so it's hereby skipped.
|
||||
@pytest.mark.skip
|
||||
def test_gsi_identical(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
@@ -74,9 +71,9 @@ def test_gsi_identical(dynamodb):
|
||||
# results (in different order).
|
||||
assert multiset(items) == multiset(full_scan(table))
|
||||
assert_index_scan(table, 'hello', items)
|
||||
# We can't scan a non-existent index
|
||||
# We can't scan a non-existant index
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
full_scan(table, ConsistentRead=False, IndexName='wrong')
|
||||
full_scan(table, IndexName='wrong')
|
||||
table.delete()
|
||||
|
||||
# One of the simplest forms of a non-trivial GSI: The base table has a hash
|
||||
@@ -153,18 +150,13 @@ def test_gsi_missing_table(dynamodb):
|
||||
dynamodb.meta.client.scan(TableName='nonexistent_table', IndexName='any_name')
|
||||
|
||||
# Verify that strongly-consistent reads on GSI are *not* allowed.
|
||||
@pytest.mark.xfail(reason="GSI strong consistency not checked")
|
||||
def test_gsi_strong_consistency(test_table_gsi_1):
|
||||
with pytest.raises(ClientError, match='ValidationException.*Consistent'):
|
||||
full_query(test_table_gsi_1, KeyConditions={'c': {'AttributeValueList': ['hi'], 'ComparisonOperator': 'EQ'}}, IndexName='hello', ConsistentRead=True)
|
||||
with pytest.raises(ClientError, match='ValidationException.*Consistent'):
|
||||
full_scan(test_table_gsi_1, IndexName='hello', ConsistentRead=True)
|
||||
|
||||
# Test that setting an indexed string column to an empty string is illegal,
|
||||
# since keys cannot contain empty strings
|
||||
def test_gsi_empty_value(test_table_gsi_2):
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_gsi_2.put_item(Item={'p': random_string(), 'x': ''})
|
||||
|
||||
# Verify that a GSI is correctly listed in describe_table
|
||||
@pytest.mark.xfail(reason="DescribeTable provides index names only, no size or item count")
|
||||
def test_gsi_describe(test_table_gsi_1):
|
||||
@@ -295,8 +287,8 @@ def test_gsi_missing_attribute(test_table_gsi_2):
|
||||
test_table_gsi_2.put_item(Item={'p': p2})
|
||||
|
||||
# Both items are now in the base table:
|
||||
assert test_table_gsi_2.get_item(Key={'p': p1}, ConsistentRead=True)['Item'] == {'p': p1, 'x': x1}
|
||||
assert test_table_gsi_2.get_item(Key={'p': p2}, ConsistentRead=True)['Item'] == {'p': p2}
|
||||
assert test_table_gsi_2.get_item(Key={'p': p1})['Item'] == {'p': p1, 'x': x1}
|
||||
assert test_table_gsi_2.get_item(Key={'p': p2})['Item'] == {'p': p2}
|
||||
|
||||
# But only the first item is in the index: It can be found using a
|
||||
# Query, and a scan of the index won't find it (but a scan on the base
|
||||
@@ -308,7 +300,7 @@ def test_gsi_missing_attribute(test_table_gsi_2):
|
||||
# and item will "never" appear in the index. We do this test last,
|
||||
# so if we had a bug and such item did appear, hopefully we had enough
|
||||
# time for the bug to become visible. At least sometimes.
|
||||
assert not any([i['p'] == p2 for i in full_scan(test_table_gsi_2, ConsistentRead=False, IndexName='hello')])
|
||||
assert not any([i['p'] == p2 for i in full_scan(test_table_gsi_2, IndexName='hello')])
|
||||
|
||||
# Test when a table has a GSI, if the indexed attribute has the wrong type,
|
||||
# the update operation is rejected, and is added to neither base table nor
|
||||
@@ -410,20 +402,20 @@ def test_gsi_missing_attribute_3(test_table_gsi_3):
|
||||
# First, add an item with a missing "a" value. It should appear in the
|
||||
# base table, but not in the index:
|
||||
test_table_gsi_3.put_item(Item={'p': p, 'b': b})
|
||||
assert test_table_gsi_3.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': b}
|
||||
assert test_table_gsi_3.get_item(Key={'p': p})['Item'] == {'p': p, 'b': b}
|
||||
# Note: with eventually consistent read, we can't really be sure that
|
||||
# an item will "never" appear in the index. We hope that if a bug exists
|
||||
# and such an item did appear, sometimes the delay here will be enough
|
||||
# for the unexpected item to become visible.
|
||||
assert not any([i['p'] == p for i in full_scan(test_table_gsi_3, ConsistentRead=False, IndexName='hello')])
|
||||
assert not any([i['p'] == p for i in full_scan(test_table_gsi_3, IndexName='hello')])
|
||||
# Same thing for an item with a missing "b" value:
|
||||
test_table_gsi_3.put_item(Item={'p': p, 'a': a})
|
||||
assert test_table_gsi_3.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': a}
|
||||
assert not any([i['p'] == p for i in full_scan(test_table_gsi_3, ConsistentRead=False, IndexName='hello')])
|
||||
assert test_table_gsi_3.get_item(Key={'p': p})['Item'] == {'p': p, 'a': a}
|
||||
assert not any([i['p'] == p for i in full_scan(test_table_gsi_3, IndexName='hello')])
|
||||
# And for an item missing both:
|
||||
test_table_gsi_3.put_item(Item={'p': p})
|
||||
assert test_table_gsi_3.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p}
|
||||
assert not any([i['p'] == p for i in full_scan(test_table_gsi_3, ConsistentRead=False, IndexName='hello')])
|
||||
assert test_table_gsi_3.get_item(Key={'p': p})['Item'] == {'p': p}
|
||||
assert not any([i['p'] == p for i in full_scan(test_table_gsi_3, IndexName='hello')])
|
||||
|
||||
# A fourth scenario of GSI. Two GSIs on a single base table.
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -735,10 +727,10 @@ def test_gsi_backfill(dynamodb):
|
||||
# assert_index_scan() or assert_index_query() functions) because after
|
||||
# we waited for backfilling to complete, we know all the pre-existing
|
||||
# data is already in the index.
|
||||
assert multiset(items1) == multiset(full_scan(table, ConsistentRead=False, IndexName='hello'))
|
||||
assert multiset(items1) == multiset(full_scan(table, IndexName='hello'))
|
||||
# We can also use Query on the new GSI, to search on the attribute x:
|
||||
assert multiset([items1[3]]) == multiset(full_query(table,
|
||||
ConsistentRead=False, IndexName='hello',
|
||||
IndexName='hello',
|
||||
KeyConditions={'x': {'AttributeValueList': [items1[3]['x']], 'ComparisonOperator': 'EQ'}}))
|
||||
# Let's also test that we cannot add another index with the same name
|
||||
# that already exists
|
||||
@@ -785,7 +777,7 @@ def test_gsi_delete(dynamodb):
|
||||
wait_for_gsi_gone(table, 'hello')
|
||||
# Now index is gone. We cannot query using it.
|
||||
with pytest.raises(ClientError, match='ValidationException.*hello'):
|
||||
full_query(table, ConsistentRead=False, IndexName='hello',
|
||||
full_query(table, IndexName='hello',
|
||||
KeyConditions={'x': {'AttributeValueList': [items[3]['x']], 'ComparisonOperator': 'EQ'}})
|
||||
table.delete()
|
||||
|
||||
@@ -22,7 +22,7 @@ import requests
|
||||
# Test that a health check can be performed with a GET packet
|
||||
def test_health_works(dynamodb):
|
||||
url = dynamodb.meta.client._endpoint.host
|
||||
response = requests.get(url, verify=False)
|
||||
response = requests.get(url)
|
||||
assert response.ok
|
||||
assert response.content.decode('utf-8').strip() == 'healthy: {}'.format(url.replace('https://', '').replace('http://', ''))
|
||||
|
||||
@@ -151,14 +151,14 @@ def test_basic_string_more_update(test_table):
|
||||
assert item['a2'] == val2
|
||||
assert not 'a3' in item
|
||||
|
||||
# Test that item operations on a non-existent table name fail with correct
|
||||
# Test that item operations on a non-existant table name fail with correct
|
||||
# error code.
|
||||
def test_item_operations_nonexistent_table(dynamodb):
|
||||
with pytest.raises(ClientError, match='ResourceNotFoundException'):
|
||||
dynamodb.meta.client.put_item(TableName='non_existent_table',
|
||||
Item={'a':{'S':'b'}})
|
||||
|
||||
# Fetching a non-existent item. According to the DynamoDB doc, "If there is no
|
||||
# Fetching a non-existant item. According to the DynamoDB doc, "If there is no
|
||||
# matching item, GetItem does not return any data and there will be no Item
|
||||
# element in the response."
|
||||
def test_get_item_missing_item(test_table):
|
||||
@@ -400,282 +400,3 @@ def test_put_item_replace(test_table_s, test_table):
|
||||
assert test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item'] == {'p': p, 'c': c, 'a': 'hi'}
|
||||
test_table.put_item(Item={'p': p, 'c': c, 'b': 'hello'})
|
||||
assert test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item'] == {'p': p, 'c': c, 'b': 'hello'}
|
||||
|
||||
# Test what UpdateItem does on a non-existent item. An operation that puts an
|
||||
# attribute, creates this item. Even an empty operation creates an item
|
||||
# (this is test_empty_update() above). But an operation that only deletes
|
||||
# attributes, does not create an empty item. This reproduces issue #5862.
|
||||
def test_update_item_non_existent(test_table_s):
|
||||
# An update that puts an attribute on a non-existent item, creates it:
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 3, 'Action': 'PUT'}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 3}
|
||||
# An update that does *nothing* on a non-existent item, still creates it:
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p}, AttributeUpdates={})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p}
|
||||
# HOWEVER, an update that only deletes an attribute on a non-existent
|
||||
# item, does NOT creates it: (issue #5862 was about Alternator wrongly
|
||||
# creating and empty item in this case).
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'DELETE'}})
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
# Test the same thing - that an attribute-deleting update does not
|
||||
# create a non-existing item - but now with the update expression syntax:
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE a')
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
|
||||
# UpdateItem's AttributeUpdate's DELETE operations has two different
|
||||
# meanings. It can be used to delete an entire attribute, but can also be
|
||||
# used to delete elements from a set attribute. The latter is a RMW operation,
|
||||
# because it requires testing the existing value of the attribute, if it
|
||||
# is indeed a set of the desired type.
|
||||
@pytest.mark.xfail(reason="UpdateItem AttributeUpdates DELETE not implemented for sets")
|
||||
def test_update_item_delete(test_table_s):
|
||||
p = random_string()
|
||||
a = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': a})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': a}
|
||||
# An Value-less DELETE just deletes the entire attribute
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'DELETE'}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p}
|
||||
# The "Value" parameter is rejected for *most* types, except sets.
|
||||
# Even lists (supported by the ADD operation) are rejected.
|
||||
for value in ['b', 3, bytearray('b', 'utf-8'), True, False, None,
|
||||
[2,3], {'a': 3}]:
|
||||
test_table_s.put_item(Item={'p': p, 'a': value})
|
||||
with pytest.raises(ClientError, match='ValidationException.*type'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'DELETE', 'Value': value}})
|
||||
# "Value" is allowed for sets, but the existing content of the attribute
|
||||
# must be a set as well, otherwise we get an error on mismatched type
|
||||
with pytest.raises(ClientError, match='ValidationException.*mismatch'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'DELETE', 'Value': set([1, 2])}})
|
||||
# When "Value" is a set and the attribute is a set of the same type,
|
||||
# DELETE remove these items from the set attribute. It works on all
|
||||
# three set types (string, bytes, number):
|
||||
test_table_s.put_item(Item={'p': p, 'a': set([1, 2, 3, 4, 5])})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'DELETE', 'Value': set([2, 4])}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': set([1, 3, 5])}
|
||||
test_table_s.put_item(Item={'p': p, 'a': set(['dog', 'cat', 'lion'])})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'DELETE', 'Value': set(['dog'])}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': set(['cat', 'lion'])}
|
||||
test_table_s.put_item(Item={'p': p, 'a': set([b'dog', b'cat', b'lion'])})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'DELETE', 'Value': set([b'cat'])}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': set([b'dog', b'lion'])}
|
||||
# If the item and value are both sets, but not of the same type, we
|
||||
# get an error on the mismatched types:
|
||||
test_table_s.put_item(Item={'p': p, 'a': set([1, 2, 3, 4, 5])})
|
||||
with pytest.raises(ClientError, match='ValidationException.*mismatch'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'DELETE', 'Value': set(['hi'])}})
|
||||
# An empty set is not allowed as Value:
|
||||
test_table_s.put_item(Item={'p': p, 'a': set([1, 2, 3])})
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'DELETE', 'Value': set([])}})
|
||||
# Deleting all the elments from a set doesn't leave an empty set
|
||||
# (which DynamoDB doesn't allow) but rather deletes the attribute:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'DELETE', 'Value': set([1, 2, 3])}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p}
|
||||
# Removing an item not already in the set is fine, and ignores it:
|
||||
test_table_s.put_item(Item={'p': p, 'a': set([1, 2, 3])})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'DELETE', 'Value': set([4])}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': set([1, 2, 3])}
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'DELETE', 'Value': set([2, 5])}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': set([1, 3])}
|
||||
# Asking to delete an attribute or parts of a set attribute is silently
|
||||
# ignored if the item doesn't exist (no error, and item isn't created).
|
||||
p = random_string()
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'DELETE'}})
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'DELETE', 'Value': set([4])}})
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
|
||||
# Test for UpdateItem's AttributeUpdate's ADD operation, which has different
|
||||
# meanings for numbers and sets - but not for other types.
|
||||
@pytest.mark.xfail(reason="UpdateItem AttributeUpdates ADD not implemented")
|
||||
def test_update_item_add(test_table_s):
|
||||
p = random_string()
|
||||
|
||||
# ADD operations on numbers:
|
||||
test_table_s.put_item(Item={'p': p, 'a': 7})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'ADD', 'Value': 2}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 9}
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'ADD', 'Value': Decimal(-3.5)}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': Decimal(5.5)}
|
||||
# Incrementing a non-existent attribute is allowed (as if the value is 0)
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Action': 'ADD', 'Value': 2}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': Decimal(5.5), 'b': 2}
|
||||
|
||||
# ADD operation on sets:
|
||||
test_table_s.put_item(Item={'p': p, 'a': set([1, 2])})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'ADD', 'Value': set([3, 4])}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': set([1, 2, 3, 4])}
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'ADD', 'Value': set([3, 5])}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': set([1, 2, 3, 4, 5])}
|
||||
# Adding a set to a non-existent attribute is allowed (as if empty set)
|
||||
# The DynamoDB documentation suggests this is only allowed for a set
|
||||
# of numbers, but it actually works for any set type.
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Action': 'ADD', 'Value': set([7])}})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'c': {'Action': 'ADD', 'Value': set(['a', 'b'])}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': set([1, 2, 3, 4, 5]), 'b': set([7]), 'c': set(['a', 'b'])}
|
||||
# The set type in the attribute and the Value argument needs to match:
|
||||
with pytest.raises(ClientError, match='ValidationException.*mismatch'):
|
||||
test_table_s.put_item(Item={'p': p, 'a': set([1, 2])})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'ADD', 'Value': set(['a'])}})
|
||||
|
||||
# ADD operation on lists (not documented, but works similar to sets!)
|
||||
test_table_s.put_item(Item={'p': p, 'a': [1, 2]})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'ADD', 'Value': [3, 4]}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': [1, 2, 3, 4]}
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'ADD', 'Value': [3, 5]}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': [1, 2, 3, 4, 3, 5]}
|
||||
# Adding a list to a non-existent attribute is allowed (as if empty list)
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Action': 'ADD', 'Value': [7]}})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'c': {'Action': 'ADD', 'Value': ['a', 'b']}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': [1, 2, 3, 4, 3, 5], 'b': [7], 'c': ['a', 'b']}
|
||||
# Unlike sets which have a homogeneous element type, lists don't, and
|
||||
# elements of any type can be appended to a list:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'c': {'Action': 'ADD', 'Value': [3]}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == ['a', 'b', 3]
|
||||
|
||||
# ADD doesn't support any other type as the value parameter.
|
||||
# In particular, it can't do things like append strings, or add items to
|
||||
# a map, or add a single string value to a set.
|
||||
with pytest.raises(ClientError, match='ValidationException.*type'):
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'dog'})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'ADD', 'Value': 's'}})
|
||||
with pytest.raises(ClientError, match='ValidationException.*type'):
|
||||
test_table_s.put_item(Item={'p': p, 'a': {'x': 1}})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'ADD', 'Value': {'y': 2}}})
|
||||
with pytest.raises(ClientError, match='ValidationException.*type'):
|
||||
test_table_s.put_item(Item={'p': p, 'a': set(['dog'])})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'ADD', 'Value': 'cat'}})
|
||||
|
||||
# If the entire item doesn't exist, ADD can create it just like we
|
||||
# tested above that it can create an attribute.
|
||||
for value in [3, set([1, 2]), set(['a', 'b']), [1, 2]]:
|
||||
test_table_s.delete_item(Key={'p': p})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Action': 'ADD', 'Value': value}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': value}
|
||||
|
||||
# DynamoDB Does not allow empty sets.
|
||||
# Trying to ask UpdateItem to PUT one of these in an attribute should be
|
||||
# forbidden. Empty lists and maps *are* allowed.
|
||||
def test_update_item_empty_attribute(test_table_s):
|
||||
p = random_string()
|
||||
# Empty sets are *not* allowed
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'c': {'Action': 'PUT', 'Value': set([])}})
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
# But empty lists, maps, strings and binary blobs *are* allowed:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'d': {'Action': 'PUT', 'Value': []}})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'e': {'Action': 'PUT', 'Value': {}}})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'f': {'Action': 'PUT', 'Value': ''}})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'g': {'Action': 'PUT', 'Value': bytearray('', 'utf-8')}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'd': [], 'e': {}, 'f': '', 'g': bytearray('', 'utf-8')}
|
||||
|
||||
# Test that empty strings are not accepted for keys
|
||||
def test_update_item_empty_key(test_table_s, test_table_b, test_table_ss, test_table_sb):
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_s.update_item(Key={'p': ''}, AttributeUpdates={'v': {'Action': 'PUT', 'Value': 'abc'}})
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_b.update_item(Key={'p': bytearray('', 'utf-8')}, AttributeUpdates={'v': {'Action': 'PUT', 'Value': 'abc'}})
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_ss.update_item(Key={'p': 'abc', 'c': ''}, AttributeUpdates={'v': {'Action': 'PUT', 'Value': 'abc'}})
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_sb.update_item(Key={'p': 'abc', 'c': bytearray('', 'utf-8')}, AttributeUpdates={'v': {'Action': 'PUT', 'Value': 'abc'}})
|
||||
|
||||
# Same as the above test (that we cannot create empty sets), but using PutItem
|
||||
def test_put_item_empty_attribute(test_table_s):
|
||||
p = random_string()
|
||||
# Empty sets are *not* allowed
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_s.put_item(Item={'p': p, 'a': set([])})
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
# But empty lists, maps, strings and binary blobs *are* allowed:
|
||||
test_table_s.put_item(Item={'p': p, 'a': [], 'b': {}, 'c': '', 'd': bytearray('', 'utf-8')})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': [], 'b': {}, 'c': '', 'd': bytearray('', 'utf-8')}
|
||||
|
||||
# Test that empty strings are not accepted for keys
|
||||
def test_put_item_empty_key(test_table_s, test_table_b, test_table_ss, test_table_sb):
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_s.put_item(Item={'p': '', 'v': 'something'})
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_b.put_item(Item={'p': bytearray('', 'utf-8'), 'v': 'something'})
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_ss.put_item(Item={'p': 'abc', 'c': '', 'v': 'something'})
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_sb.put_item(Item={'p': 'abc', 'c': bytearray('', 'utf-8'), 'v': 'something'})
|
||||
|
||||
# In many other tests, we tested that ExpressionAttributeNames/Values
|
||||
# entries which aren't used in an the different kinds of expressions, are
|
||||
# detected and cause an error. Here we verify that also if there is no
|
||||
# expression at all, ExpressionAttributeNames/Values must not be present.
|
||||
def test_unused_entries_no_expression(test_table_s):
|
||||
p = random_string()
|
||||
# PutItem:
|
||||
with pytest.raises(ClientError, match='ValidationException.*ExpressionAttributeNames'):
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'dog'},
|
||||
ExpressionAttributeNames={'#name1': 'x'})
|
||||
with pytest.raises(ClientError, match='ValidationException.*ExpressionAttributeValues'):
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'dog'},
|
||||
ExpressionAttributeValues={':val1': 1})
|
||||
# DeleteItem:
|
||||
with pytest.raises(ClientError, match='ValidationException.*ExpressionAttributeNames'):
|
||||
test_table_s.delete_item(Key={'p': p},
|
||||
ExpressionAttributeNames={'#name1': 'x'})
|
||||
with pytest.raises(ClientError, match='ValidationException.*ExpressionAttributeValues'):
|
||||
test_table_s.delete_item(Key={'p': p},
|
||||
ExpressionAttributeValues={':val1': 1})
|
||||
# UpdateItem:
|
||||
with pytest.raises(ClientError, match='ValidationException.*ExpressionAttributeNames'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 'dog', 'Action': 'PUT'}},
|
||||
ExpressionAttributeNames={'#name1': 'x'})
|
||||
with pytest.raises(ClientError, match='ValidationException.*ExpressionAttributeValues'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 'dog', 'Action': 'PUT'}},
|
||||
ExpressionAttributeValues={':val1': 1})
|
||||
# GetItem. We can't test ExpressionAttributeValues with boto3 (which
|
||||
# doesn't allow this parameter to get_item().
|
||||
with pytest.raises(ClientError, match='ValidationException.*ExpressionAttributeNames'):
|
||||
test_table_s.get_item(Key={'p': p},
|
||||
ExpressionAttributeNames={'#name1': 'x'})
|
||||
@@ -26,22 +26,25 @@ import time
|
||||
from botocore.exceptions import ClientError, ParamValidationError
|
||||
from util import create_test_table, random_string, full_scan, full_query, multiset, list_tables
|
||||
|
||||
# LSIs support strongly-consistent reads, so the following functions do not
|
||||
# need to retry like we did in test_gsi.py for GSIs:
|
||||
# Currently, Alternator's LSIs only support eventually consistent reads, so tests
|
||||
# that involve writing to a table and then expect to read something from it cannot
|
||||
# be guaranteed to succeed without retrying the read. The following utility
|
||||
# functions make it easy to write such tests.
|
||||
def assert_index_query(table, index_name, expected_items, **kwargs):
|
||||
assert multiset(expected_items) == multiset(full_query(table, IndexName=index_name, **kwargs))
|
||||
def assert_index_scan(table, index_name, expected_items, **kwargs):
|
||||
assert multiset(expected_items) == multiset(full_scan(table, IndexName=index_name, **kwargs))
|
||||
|
||||
# A version doing retries instead of ConsistentRead, to be used just for the
|
||||
# one test below which has both GSI and LSI:
|
||||
def retrying_assert_index_query(table, index_name, expected_items, **kwargs):
|
||||
for i in range(3):
|
||||
if multiset(expected_items) == multiset(full_query(table, IndexName=index_name, ConsistentRead=False, **kwargs)):
|
||||
if multiset(expected_items) == multiset(full_query(table, IndexName=index_name, **kwargs)):
|
||||
return
|
||||
print('retrying_assert_index_query retrying')
|
||||
print('assert_index_query retrying')
|
||||
time.sleep(1)
|
||||
assert multiset(expected_items) == multiset(full_query(table, IndexName=index_name, ConsistentRead=False, **kwargs))
|
||||
assert multiset(expected_items) == multiset(full_query(table, IndexName=index_name, **kwargs))
|
||||
|
||||
def assert_index_scan(table, index_name, expected_items, **kwargs):
|
||||
for i in range(3):
|
||||
if multiset(expected_items) == multiset(full_scan(table, IndexName=index_name, **kwargs)):
|
||||
return
|
||||
print('assert_index_scan retrying')
|
||||
time.sleep(1)
|
||||
assert multiset(expected_items) == multiset(full_scan(table, IndexName=index_name, **kwargs))
|
||||
|
||||
# Although quite silly, it is actually allowed to create an index which is
|
||||
# identical to the base table.
|
||||
@@ -63,7 +66,7 @@ def test_lsi_identical(dynamodb):
|
||||
# results (in different order).
|
||||
assert multiset(items) == multiset(full_scan(table))
|
||||
assert_index_scan(table, 'hello', items)
|
||||
# We can't scan a non-existent index
|
||||
# We can't scan a non-existant index
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
full_scan(table, IndexName='wrong')
|
||||
table.delete()
|
||||
@@ -125,9 +128,8 @@ def test_lsi_wrong(dynamodb):
|
||||
])
|
||||
table.delete()
|
||||
|
||||
# A simple scenario for LSI. Base table has a partition key and a sort key,
|
||||
# index has the same partition key key but a different sort key - one of
|
||||
# the non-key attributes from the base table.
|
||||
# A simple scenario for LSI. Base table has just hash key, Index has an
|
||||
# additional sort key - one of the non-key attributes from the base table.
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_lsi_1(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
@@ -213,12 +215,6 @@ def test_lsi_4(test_table_lsi_4):
|
||||
KeyConditions={'p': {'AttributeValueList': [i5], 'ComparisonOperator': 'EQ'},
|
||||
column: {'AttributeValueList': [i5], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
# Test that setting an indexed string column to an empty string is illegal,
|
||||
# since keys cannot contain empty strings
|
||||
def test_lsi_empty_value(test_table_lsi_1):
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_lsi_1.put_item(Item={'p': random_string(), 'c': random_string(), 'b': ''})
|
||||
|
||||
def test_lsi_describe(test_table_lsi_4):
|
||||
desc = test_table_lsi_4.meta.client.describe_table(TableName=test_table_lsi_4.name)
|
||||
assert 'Table' in desc
|
||||
@@ -306,11 +302,13 @@ def test_lsi_consistent_read(test_table_lsi_1):
|
||||
expected_items = [i for i in items if i['p'] == p1 and i['b'] == b1]
|
||||
assert_index_query(test_table_lsi_1, 'hello', expected_items,
|
||||
KeyConditions={'p': {'AttributeValueList': [p1], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b1], 'ComparisonOperator': 'EQ'}})
|
||||
'b': {'AttributeValueList': [b1], 'ComparisonOperator': 'EQ'}},
|
||||
ConsistentRead=True)
|
||||
expected_items = [i for i in items if i['p'] == p2 and i['b'] == b2]
|
||||
assert_index_query(test_table_lsi_1, 'hello', expected_items,
|
||||
KeyConditions={'p': {'AttributeValueList': [p2], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b2], 'ComparisonOperator': 'EQ'}})
|
||||
'b': {'AttributeValueList': [b2], 'ComparisonOperator': 'EQ'}},
|
||||
ConsistentRead=True)
|
||||
|
||||
# A table with both gsi and lsi present
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -362,36 +360,6 @@ def test_lsi_and_gsi(test_table_lsi_gsi):
|
||||
|
||||
for index in ['hello_g1', 'hello_l1']:
|
||||
expected_items = [i for i in items if i['p'] == p1 and i['x1'] == x1]
|
||||
retrying_assert_index_query(test_table_lsi_gsi, index, expected_items,
|
||||
assert_index_query(test_table_lsi_gsi, index, expected_items,
|
||||
KeyConditions={'p': {'AttributeValueList': [p1], 'ComparisonOperator': 'EQ'},
|
||||
'x1': {'AttributeValueList': [x1], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
# This test is a version of test_filter_expression_and_projection_expression
|
||||
# from test_filter_expression, which involves a Query which projects only
|
||||
# one column but filters on another one, and the point is to verify that
|
||||
# the implementation got also the filtered column (for the filtering to work)
|
||||
# but did not return it with the results. This version does the same, except
|
||||
# that either the filtered column, or the projected column, is an LSI key.
|
||||
# In our implementation, LSI keys are implemented differently from ordinary
|
||||
# attributes - they are real Scylla columns and not just items in the
|
||||
# ":attrs" map - so this test checks that our implementation of the filtering
|
||||
# and projection (and their combination) did not mess up this special case.
|
||||
# This test reproduces issue #6951.
|
||||
def test_lsi_filter_expression_and_projection_expression(test_table_lsi_1):
|
||||
p = random_string()
|
||||
test_table_lsi_1.put_item(Item={'p': p, 'c': 'hi', 'b': 'dog', 'y': 'cat'})
|
||||
test_table_lsi_1.put_item(Item={'p': p, 'c': 'yo', 'b': 'mouse', 'y': 'horse'})
|
||||
# Case 1: b (the LSI key) is in filter but not in projection:
|
||||
got_items = full_query(test_table_lsi_1,
|
||||
KeyConditionExpression='p=:p',
|
||||
FilterExpression='b=:b',
|
||||
ProjectionExpression='y',
|
||||
ExpressionAttributeValues={':p': p, ':b': 'mouse'})
|
||||
assert(got_items == [{'y': 'horse'}])
|
||||
# Case 2: b (the LSI key) is in the projection, but not the filter:
|
||||
got_items = full_query(test_table_lsi_1,
|
||||
KeyConditionExpression='p=:p',
|
||||
FilterExpression='y=:y',
|
||||
ProjectionExpression='b',
|
||||
ExpressionAttributeValues={':p': p, ':y': 'cat'})
|
||||
assert(got_items == [{'b': 'dog'}])
|
||||
@@ -134,10 +134,10 @@ def test_projection_expression_path(test_table_s):
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b[0]')['Item'] == {'a': {'b': [2]}}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b[2]')['Item'] == {'a': {'b': [{'x': 'hi', 'y': 'yo'}]}}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b[2].y')['Item'] == {'a': {'b': [{'y': 'yo'}]}}
|
||||
# Trying to read any sort of non-existent attribute returns an empty item.
|
||||
# Trying to read any sort of non-existant attribute returns an empty item.
|
||||
# This includes a non-existing top-level attribute, an attempt to read
|
||||
# beyond the end of an array or a non-existent member of a dictionary, as
|
||||
# well as paths which begin with a non-existent prefix.
|
||||
# beyond the end of an array or a non-existant member of a dictionary, as
|
||||
# well as paths which begin with a non-existant prefix.
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='x')['Item'] == {}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b[3]')['Item'] == {}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.x')['Item'] == {}
|
||||
@@ -147,7 +147,6 @@ def test_projection_expression_path(test_table_s):
|
||||
# structured the same was as in the original item:
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b[0],a.b[1]')['Item'] == {'a': {'b': [2, 4]}}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b[0],a.c')['Item'] == {'a': {'b': [2], 'c': 5}}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.c,b')['Item'] == {'a': {'c': 5}, 'b': 'hello'}
|
||||
# It is not allowed to read the same path multiple times. The error from
|
||||
# DynamoDB looks like: "Invalid ProjectionExpression: Two document paths
|
||||
# overlap with each other; must remove or rewrite one of these paths;
|
||||
@@ -160,59 +159,6 @@ def test_projection_expression_path(test_table_s):
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a,a.b[0]')['Item']
|
||||
|
||||
# Above in test_projection_expression_toplevel_syntax() we tested how
|
||||
# name references (#name) work in top-level attributes. In the following
|
||||
# two tests we test how they work in more elaborate paths:
|
||||
# 1. Multiple path components can make multiple references, e.g., "#a.#b"
|
||||
# 2. Conversely, a single reference, e.g., "#a", is always a single path
|
||||
# component. Even if "#a" is "a.b", this refers to the literal attribute
|
||||
# "a.b" - with a dot in its name - and not to the b element in a.
|
||||
@pytest.mark.xfail(reason="ProjectionExpression does not yet support attribute paths")
|
||||
def test_projection_expression_path_references(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': {'b': 1, 'c': 2}, 'b': 'hi'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b')['Item'] == {'a': {'b': 1}}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='#n1.b', ExpressionAttributeNames={'#n1': 'a'})['Item'] == {'a': {'b': 1}}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.#n2', ExpressionAttributeNames={'#n2': 'b'})['Item'] == {'a': {'b': 1}}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='#n1.#n2', ExpressionAttributeNames={'#n1': 'a', '#n2': 'b'})['Item'] == {'a': {'b': 1}}
|
||||
# Missing or unused names in ExpressionAttributeNames are errors:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.#n2', ExpressionAttributeNames={'#wrong': 'b'})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.#n2', ExpressionAttributeNames={'#n2': 'b', '#unused': 'x'})
|
||||
|
||||
@pytest.mark.xfail(reason="ProjectionExpression does not yet support attribute paths")
|
||||
def test_projection_expression_path_dot(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a.b': 'hi', 'a': {'b': 'yo'}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b')['Item'] == {'a': {'b': 'yo'}}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='#name', ExpressionAttributeNames={'#name': 'a.b'})['Item'] == {'a.b': 'hi'}
|
||||
|
||||
# DynamoDB does not allow "overlapping" paths to be listed in
|
||||
# ProjectionExpression. This includes both identical paths, and paths where
|
||||
# one is a sub-path of the other - e.g. "a.b" and "a.b.c". As we already saw
|
||||
# above, paths with just a common *prefix* - e.g., "a.b, a.c" - are fine.
|
||||
@pytest.mark.xfail(reason="ProjectionExpression does not yet support attribute paths")
|
||||
def test_projection_expression_path_overlap(test_table_s):
|
||||
# The overlap is tested symbolically, on the given paths, without any
|
||||
# relation to what the item contains, or whether it even exists. So we
|
||||
# don't even need to create an item for this test. We still need a
|
||||
# key for the GetItem call :-)
|
||||
p = random_string()
|
||||
for expr in ['a, a',
|
||||
'a.b, a.b',
|
||||
'a[1], a[1]',
|
||||
'a, a.b',
|
||||
'a.b, a',
|
||||
'a.b, a.b[2]',
|
||||
'a.b, a.b.c',
|
||||
'a, a.b[2].c',
|
||||
]:
|
||||
with pytest.raises(ClientError, match='ValidationException.* overlap'):
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression=expr)
|
||||
|
||||
# Above we nested paths in ProjectionExpression, but just for the GetItem
|
||||
# request. Let's verify they also work in Query and Scan requests:
|
||||
@pytest.mark.xfail(reason="ProjectionExpression does not yet support attribute paths")
|
||||
def test_query_projection_expression_path(test_table):
|
||||
p = random_string()
|
||||
@@ -239,22 +185,6 @@ def test_scan_projection_expression_path(test_table):
|
||||
expected_items = [{'p': p, 'a': {'x': x['a']['x']}} for x in items]
|
||||
assert multiset(expected_items) == multiset(got_items)
|
||||
|
||||
# BatchGetItem also supports ProjectionExpression, let's test that it
|
||||
# applies to all items, and that it correctly suports document paths as well.
|
||||
@pytest.mark.xfail(reason="ProjectionExpression does not yet support attribute paths")
|
||||
def test_batch_get_item_projection_expression_path(test_table_s):
|
||||
items = [{'p': random_string(), 'a': {'b': random_string()}, 'c': random_string()} for i in range(3)]
|
||||
with test_table_s.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
got_items = test_table_s.meta.client.batch_get_item(
|
||||
RequestItems = {test_table_s.name: {
|
||||
'Keys': [{'p': item['p']} for item in items],
|
||||
'ProjectionExpression': 'a.b',
|
||||
'ConsistentRead': True}})['Responses'][test_table_s.name]
|
||||
expected_items = [{'a': {'b': item['a']['b']}} for item in items]
|
||||
assert multiset(got_items) == multiset(expected_items)
|
||||
|
||||
# It is not allowed to use both ProjectionExpression and its older cousin,
|
||||
# AttributesToGet, together. If trying to do this, DynamoDB produces an error
|
||||
# like "Can not use both expression and non-expression parameters in the same
|
||||
@@ -269,19 +199,3 @@ def test_projection_expression_and_attributes_to_get(test_table_s):
|
||||
full_scan(test_table_s, ProjectionExpression='a', AttributesToGet=['a'])
|
||||
with pytest.raises(ClientError, match='ValidationException.*both'):
|
||||
full_query(test_table_s, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, ProjectionExpression='a', AttributesToGet=['a'])
|
||||
|
||||
# above in test_projection_expression_toplevel_syntax among other things
|
||||
# we noted how spurious entries in ExpressionAttributeNames, not needed
|
||||
# the the ProjectionExpression, cause an error. Sometimes we have two
|
||||
# expressions in the same request, for example, both a ProjectionExpression
|
||||
# and a KeyConditionExpression. It's only an error if a name is not
|
||||
# needed by both of these expressions
|
||||
def test_projection_expression_and_key_condition_expression(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hello', 'b': 'hi'})
|
||||
got_items = full_query(test_table_s,
|
||||
KeyConditionExpression='#name1 = :val1',
|
||||
ProjectionExpression='#name2',
|
||||
ExpressionAttributeNames={'#name1': 'p', '#name2': 'a'},
|
||||
ExpressionAttributeValues={':val1': p});
|
||||
assert got_items == [{'a': 'hello'}]
|
||||
@@ -17,11 +17,6 @@
|
||||
# along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Tests for the Query operation
|
||||
# Some of the Query features are tested in separate files:
|
||||
# * test_key_conditions.py: the KeyConditions paramter.
|
||||
# * test_key_condition_expression.py: the KeyConditionExpression parameter.
|
||||
# * test_filter_expression.py: the FilterExpression parameter.
|
||||
# * test_query_filter.py: the QueryFilter parameter.
|
||||
|
||||
import random
|
||||
import pytest
|
||||
@@ -30,13 +25,126 @@ from decimal import Decimal
|
||||
from util import random_string, random_bytes, full_query, multiset
|
||||
from boto3.dynamodb.conditions import Key, Attr
|
||||
|
||||
def test_query_nonexistent_table(dynamodb):
|
||||
client = dynamodb.meta.client
|
||||
with pytest.raises(ClientError, match="ResourceNotFoundException"):
|
||||
client.query(TableName="i_do_not_exist", KeyConditions={
|
||||
# Test that scanning works fine with in-stock paginator
|
||||
def test_query_basic_restrictions(dynamodb, filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
paginator = dynamodb.meta.client.get_paginator('query')
|
||||
|
||||
# EQ
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'}
|
||||
}):
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long']) == multiset(got_items)
|
||||
|
||||
# LT
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'},
|
||||
'c' : {'AttributeValueList': ['12'], 'ComparisonOperator': 'LT'}
|
||||
}):
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['c'] < '12']) == multiset(got_items)
|
||||
|
||||
# LE
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'},
|
||||
'c' : {'AttributeValueList': ['14'], 'ComparisonOperator': 'LE'}
|
||||
}):
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['c'] <= '14']) == multiset(got_items)
|
||||
|
||||
# GT
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'},
|
||||
'c' : {'AttributeValueList': ['15'], 'ComparisonOperator': 'GT'}
|
||||
}):
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['c'] > '15']) == multiset(got_items)
|
||||
|
||||
# GE
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'},
|
||||
'c' : {'AttributeValueList': ['14'], 'ComparisonOperator': 'GE'}
|
||||
}):
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['c'] >= '14']) == multiset(got_items)
|
||||
|
||||
# BETWEEN
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'},
|
||||
'c' : {'AttributeValueList': ['155', '164'], 'ComparisonOperator': 'BETWEEN'}
|
||||
}):
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['c'] >= '155' and item['c'] <= '164']) == multiset(got_items)
|
||||
|
||||
# BEGINS_WITH
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'},
|
||||
'c' : {'AttributeValueList': ['11'], 'ComparisonOperator': 'BEGINS_WITH'}
|
||||
})
|
||||
}):
|
||||
print([item for item in items if item['p'] == 'long' and item['c'].startswith('11')])
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['c'].startswith('11')]) == multiset(got_items)
|
||||
|
||||
# Test that KeyConditionExpression parameter is supported
|
||||
@pytest.mark.xfail(reason="KeyConditionExpression not supported yet")
|
||||
def test_query_key_condition_expression(dynamodb, filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
paginator = dynamodb.meta.client.get_paginator('query')
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditionExpression=Key("p").eq("long") & Key("c").lt("12")):
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['c'] < '12']) == multiset(got_items)
|
||||
|
||||
def test_begins_with(dynamodb, test_table):
|
||||
paginator = dynamodb.meta.client.get_paginator('query')
|
||||
items = [{'p': 'unorthodox_chars', 'c': sort_key, 'str': 'a'} for sort_key in [u'ÿÿÿ', u'cÿbÿ', u'cÿbÿÿabg'] ]
|
||||
with test_table.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
|
||||
# TODO(sarna): Once bytes type is supported, /xFF character should be tested
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['unorthodox_chars'], 'ComparisonOperator': 'EQ'},
|
||||
'c' : {'AttributeValueList': [u'ÿÿ'], 'ComparisonOperator': 'BEGINS_WITH'}
|
||||
}):
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert sorted([d['c'] for d in got_items]) == sorted([d['c'] for d in items if d['c'].startswith(u'ÿÿ')])
|
||||
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['unorthodox_chars'], 'ComparisonOperator': 'EQ'},
|
||||
'c' : {'AttributeValueList': [u'cÿbÿ'], 'ComparisonOperator': 'BEGINS_WITH'}
|
||||
}):
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert sorted([d['c'] for d in got_items]) == sorted([d['c'] for d in items if d['c'].startswith(u'cÿbÿ')])
|
||||
|
||||
def test_begins_with_wrong_type(dynamodb, test_table_sn):
|
||||
paginator = dynamodb.meta.client.get_paginator('query')
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
for page in paginator.paginate(TableName=test_table_sn.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['unorthodox_chars'], 'ComparisonOperator': 'EQ'},
|
||||
'c' : {'AttributeValueList': [17], 'ComparisonOperator': 'BEGINS_WITH'}
|
||||
}):
|
||||
pass
|
||||
|
||||
# Items returned by Query should be sorted by the sort key. The following
|
||||
# tests verify that this is indeed the case, for the three allowed key types:
|
||||
@@ -107,8 +215,6 @@ def test_query_sort_order_number(test_table_sn):
|
||||
got_sort_keys = [x['c'] for x in got_items]
|
||||
assert got_sort_keys == numbers
|
||||
|
||||
# Note: this is a very partial check for the QueryFilter feature. See
|
||||
# test_query_filter.py for much more exhaustive tests for this feature.
|
||||
def test_query_filtering_attributes_equality(filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
|
||||
@@ -138,6 +244,7 @@ def test_query_filtering_attributes_equality(filled_test_table):
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['attribute'] == 'xxxx' and item['another'] == 'yy']) == multiset(got_items)
|
||||
|
||||
# Test that FilterExpression works as expected
|
||||
@pytest.mark.xfail(reason="FilterExpression not supported yet")
|
||||
def test_query_filter_expression(filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
|
||||
@@ -149,6 +256,33 @@ def test_query_filter_expression(filled_test_table):
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['attribute'] == 'xxxx' and item['another'] == 'yy']) == multiset(got_items)
|
||||
|
||||
# QueryFilter can only contain non-key attributes in order to be compatible
|
||||
def test_query_filtering_key_equality(filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
query_filter = {
|
||||
"c" : {
|
||||
"AttributeValueList" : [ "5" ],
|
||||
"ComparisonOperator": "EQ"
|
||||
}
|
||||
}
|
||||
got_items = full_query(test_table, KeyConditions={'p': {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'}}, QueryFilter=query_filter)
|
||||
print(got_items)
|
||||
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
query_filter = {
|
||||
"attribute" : {
|
||||
"AttributeValueList" : [ "x" ],
|
||||
"ComparisonOperator": "EQ"
|
||||
},
|
||||
"p" : {
|
||||
"AttributeValueList" : [ "5" ],
|
||||
"ComparisonOperator": "EQ"
|
||||
}
|
||||
}
|
||||
got_items = full_query(test_table, KeyConditions={'p': {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'}}, QueryFilter=query_filter)
|
||||
print(got_items)
|
||||
|
||||
# Test Query with the AttributesToGet parameter. Result should include the
|
||||
# selected attributes only - if one wants the key attributes as well, one
|
||||
@@ -238,14 +372,14 @@ def test_query_select(test_table_sn):
|
||||
batch.put_item(item)
|
||||
# Verify that we get back the numbers in their sorted order. By default,
|
||||
# query returns all attributes:
|
||||
got_items = test_table_sn.query(ConsistentRead=True, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}})['Items']
|
||||
got_items = test_table_sn.query(KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}})['Items']
|
||||
got_sort_keys = [x['c'] for x in got_items]
|
||||
assert got_sort_keys == numbers
|
||||
got_x_attributes = [x['x'] for x in got_items]
|
||||
assert got_x_attributes == numbers
|
||||
# Select=ALL_ATTRIBUTES does exactly the same as the default - return
|
||||
# all attributes:
|
||||
got_items = test_table_sn.query(ConsistentRead=True, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Select='ALL_ATTRIBUTES')['Items']
|
||||
got_items = test_table_sn.query(KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Select='ALL_ATTRIBUTES')['Items']
|
||||
got_sort_keys = [x['c'] for x in got_items]
|
||||
assert got_sort_keys == numbers
|
||||
got_x_attributes = [x['x'] for x in got_items]
|
||||
@@ -253,28 +387,28 @@ def test_query_select(test_table_sn):
|
||||
# Select=ALL_PROJECTED_ATTRIBUTES is not allowed on a base table (it
|
||||
# is just for indexes, when IndexName is specified)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_sn.query(ConsistentRead=True, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Select='ALL_PROJECTED_ATTRIBUTES')
|
||||
test_table_sn.query(KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Select='ALL_PROJECTED_ATTRIBUTES')
|
||||
# Select=SPECIFIC_ATTRIBUTES requires that either a AttributesToGet
|
||||
# or ProjectionExpression appears, but then really does nothing:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_sn.query(ConsistentRead=True, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Select='SPECIFIC_ATTRIBUTES')
|
||||
got_items = test_table_sn.query(ConsistentRead=True, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Select='SPECIFIC_ATTRIBUTES', AttributesToGet=['x'])['Items']
|
||||
test_table_sn.query(KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Select='SPECIFIC_ATTRIBUTES')
|
||||
got_items = test_table_sn.query(KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Select='SPECIFIC_ATTRIBUTES', AttributesToGet=['x'])['Items']
|
||||
expected_items = [{'x': i} for i in numbers]
|
||||
assert got_items == expected_items
|
||||
got_items = test_table_sn.query(ConsistentRead=True, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Select='SPECIFIC_ATTRIBUTES', ProjectionExpression='x')['Items']
|
||||
got_items = test_table_sn.query(KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Select='SPECIFIC_ATTRIBUTES', ProjectionExpression='x')['Items']
|
||||
assert got_items == expected_items
|
||||
# Select=COUNT just returns a count - not any items
|
||||
got = test_table_sn.query(ConsistentRead=True, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Select='COUNT')
|
||||
got = test_table_sn.query(KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Select='COUNT')
|
||||
assert got['Count'] == len(numbers)
|
||||
assert not 'Items' in got
|
||||
# Check again that we also get a count - not just with Select=COUNT,
|
||||
# but without Select=COUNT we also get the items:
|
||||
got = test_table_sn.query(ConsistentRead=True, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}})
|
||||
got = test_table_sn.query(KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}})
|
||||
assert got['Count'] == len(numbers)
|
||||
assert 'Items' in got
|
||||
# Select with some unknown string generates a validation exception:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_sn.query(ConsistentRead=True, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Select='UNKNOWN')
|
||||
test_table_sn.query(KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Select='UNKNOWN')
|
||||
|
||||
# Test that the "Limit" parameter can be used to return only some of the
|
||||
# items in a single partition. The items returned are the first in the
|
||||
@@ -290,20 +424,20 @@ def test_query_limit(test_table_sn):
|
||||
# Verify that we get back the numbers in their sorted order.
|
||||
# First, no Limit so we should get all numbers (we have few of them, so
|
||||
# it all fits in the default 1MB limitation)
|
||||
got_items = test_table_sn.query(ConsistentRead=True, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}})['Items']
|
||||
got_items = test_table_sn.query(KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}})['Items']
|
||||
got_sort_keys = [x['c'] for x in got_items]
|
||||
assert got_sort_keys == numbers
|
||||
# Now try a few different Limit values, and verify that the query
|
||||
# returns exactly the first Limit sorted numbers.
|
||||
for limit in [1, 2, 3, 7, 10, 17, 100, 10000]:
|
||||
got_items = test_table_sn.query(ConsistentRead=True, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Limit=limit)['Items']
|
||||
got_items = test_table_sn.query(KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Limit=limit)['Items']
|
||||
assert len(got_items) == min(limit, len(numbers))
|
||||
got_sort_keys = [x['c'] for x in got_items]
|
||||
assert got_sort_keys == numbers[0:limit]
|
||||
# Unfortunately, the boto3 library forbids a Limit of 0 on its own,
|
||||
# before even sending a request, so we can't test how the server responds.
|
||||
with pytest.raises(ParamValidationError):
|
||||
test_table_sn.query(ConsistentRead=True, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Limit=0)
|
||||
test_table_sn.query(KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Limit=0)
|
||||
|
||||
# In test_query_limit we tested just that Limit allows to stop the result
|
||||
# after right right number of items. Here we test that such a stopped result
|
||||
@@ -328,6 +462,7 @@ def test_query_limit_paging(test_table_sn):
|
||||
# return items sorted in reverse order. Combining this with Limit can
|
||||
# be used to return the last items instead of the first items of the
|
||||
# partition.
|
||||
@pytest.mark.xfail(reason="ScanIndexForward not supported yet")
|
||||
def test_query_reverse(test_table_sn):
|
||||
numbers = [Decimal(i) for i in range(20)]
|
||||
# Insert these numbers, in random order, into one partition:
|
||||
@@ -341,27 +476,28 @@ def test_query_reverse(test_table_sn):
|
||||
# First, no Limit so we should get all numbers (we have few of them, so
|
||||
# it all fits in the default 1MB limitation)
|
||||
reversed_numbers = list(reversed(numbers))
|
||||
got_items = test_table_sn.query(ConsistentRead=True, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, ScanIndexForward=True)['Items']
|
||||
got_items = test_table_sn.query(KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, ScanIndexForward=True)['Items']
|
||||
got_sort_keys = [x['c'] for x in got_items]
|
||||
assert got_sort_keys == numbers
|
||||
got_items = test_table_sn.query(ConsistentRead=True, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, ScanIndexForward=False)['Items']
|
||||
got_items = test_table_sn.query(KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, ScanIndexForward=False)['Items']
|
||||
got_sort_keys = [x['c'] for x in got_items]
|
||||
assert got_sort_keys == reversed_numbers
|
||||
# Now try a few different Limit values, and verify that the query
|
||||
# returns exactly the first Limit sorted numbers - in regular or
|
||||
# reverse order, depending on ScanIndexForward.
|
||||
for limit in [1, 2, 3, 7, 10, 17, 100, 10000]:
|
||||
got_items = test_table_sn.query(ConsistentRead=True, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Limit=limit, ScanIndexForward=True)['Items']
|
||||
got_items = test_table_sn.query(KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Limit=limit, ScanIndexForward=True)['Items']
|
||||
assert len(got_items) == min(limit, len(numbers))
|
||||
got_sort_keys = [x['c'] for x in got_items]
|
||||
assert got_sort_keys == numbers[0:limit]
|
||||
got_items = test_table_sn.query(ConsistentRead=True, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Limit=limit, ScanIndexForward=False)['Items']
|
||||
got_items = test_table_sn.query(KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, Limit=limit, ScanIndexForward=False)['Items']
|
||||
assert len(got_items) == min(limit, len(numbers))
|
||||
got_sort_keys = [x['c'] for x in got_items]
|
||||
assert got_sort_keys == reversed_numbers[0:limit]
|
||||
|
||||
# Test that paging also works properly with reverse order
|
||||
# (ScanIndexForward=false), i.e., reverse-order queries can be resumed
|
||||
@pytest.mark.xfail(reason="ScanIndexForward not supported yet")
|
||||
def test_query_reverse_paging(test_table_sn):
|
||||
numbers = [Decimal(i) for i in range(20)]
|
||||
# Insert these numbers, in random order, into one partition:
|
||||
@@ -378,11 +514,3 @@ def test_query_reverse_paging(test_table_sn):
|
||||
got_items = full_query(test_table_sn, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, ScanIndexForward=False, Limit=limit)
|
||||
got_sort_keys = [x['c'] for x in got_items]
|
||||
assert got_sort_keys == reversed_numbers
|
||||
|
||||
# A query without a KeyConditions or KeyConditionExpress is, or an empty
|
||||
# one, is obviously not allowed:
|
||||
def test_query_missing_key(test_table):
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
full_query(test_table, KeyConditions={})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
full_query(test_table)
|
||||
@@ -58,6 +58,7 @@ def test_trivial_returnvalues(test_table_s):
|
||||
# Test the ReturnValues parameter on a PutItem operation. Only two settings
|
||||
# are supported for this parameter for this operation: NONE (the default)
|
||||
# and ALL_OLD.
|
||||
@pytest.mark.xfail(reason="ReturnValues not supported")
|
||||
def test_put_item_returnvalues(test_table_s):
|
||||
# By default, the previous value of an item is not returned:
|
||||
p = random_string()
|
||||
@@ -75,11 +76,6 @@ def test_put_item_returnvalues(test_table_s):
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hi'})
|
||||
ret=test_table_s.put_item(Item={'p': p, 'a': 'hello'}, ReturnValues='ALL_OLD')
|
||||
assert ret['Attributes'] == {'p': p, 'a': 'hi'}
|
||||
# If the item does not previously exist, "Attributes" is not returned
|
||||
# at all:
|
||||
p = random_string()
|
||||
ret=test_table_s.put_item(Item={'p': p, 'a': 'hello'}, ReturnValues='ALL_OLD')
|
||||
assert not 'Attributes' in ret
|
||||
# Other ReturnValue options - UPDATED_OLD, ALL_NEW, UPDATED_NEW,
|
||||
# are supported by other operations but not by PutItem:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
@@ -99,6 +95,7 @@ def test_put_item_returnvalues(test_table_s):
|
||||
# Test the ReturnValues parameter on a DeleteItem operation. Only two settings
|
||||
# are supported for this parameter for this operation: NONE (the default)
|
||||
# and ALL_OLD.
|
||||
@pytest.mark.xfail(reason="ReturnValues not supported")
|
||||
def test_delete_item_returnvalues(test_table_s):
|
||||
# By default, the previous value of an item is not returned:
|
||||
p = random_string()
|
||||
@@ -116,11 +113,6 @@ def test_delete_item_returnvalues(test_table_s):
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hi'})
|
||||
ret=test_table_s.delete_item(Key={'p': p}, ReturnValues='ALL_OLD')
|
||||
assert ret['Attributes'] == {'p': p, 'a': 'hi'}
|
||||
# If the item does not previously exist, "Attributes" is not returned
|
||||
# at all:
|
||||
p = random_string()
|
||||
ret=test_table_s.delete_item(Key={'p': p}, ReturnValues='ALL_OLD')
|
||||
assert not 'Attributes' in ret
|
||||
# Other ReturnValue options - UPDATED_OLD, ALL_NEW, UPDATED_NEW,
|
||||
# are supported by other operations but not by PutItem:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
@@ -140,10 +132,8 @@ def test_delete_item_returnvalues(test_table_s):
|
||||
# Test the ReturnValues parameter on a UpdateItem operation. All five
|
||||
# settings are supported for this parameter for this operation: NONE
|
||||
# (the default), ALL_OLD, UPDATED_OLD, ALL_NEW and UPDATED_NEW.
|
||||
# We test them in separate tests to allow for this feature to be
|
||||
# implemented incrementally.
|
||||
|
||||
def test_update_item_returnvalues_none(test_table_s):
|
||||
@pytest.mark.xfail(reason="ReturnValues not supported")
|
||||
def test_update_item_returnvalues(test_table_s):
|
||||
# By default, the previous value of an item is not returned:
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hi', 'b': 'dog'})
|
||||
@@ -160,20 +150,6 @@ def test_update_item_returnvalues_none(test_table_s):
|
||||
ExpressionAttributeValues={':val': 'cat'})
|
||||
assert not 'Attributes' in ret
|
||||
|
||||
# The ReturnValues value is case sensitive, so while "NONE" is supported
|
||||
# (and tested above), "none" isn't:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, ReturnValues='none',
|
||||
UpdateExpression='SET a = :val',
|
||||
ExpressionAttributeValues={':val': 1})
|
||||
|
||||
# A non-supported setting "DOG" also returns in error:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, ReturnValues='DOG',
|
||||
UpdateExpression='SET a = :val',
|
||||
ExpressionAttributeValues={':val': 1})
|
||||
|
||||
def test_update_item_returnvalues_all_old(test_table_s):
|
||||
# With ReturnValues=ALL_OLD, the entire old value of the item (even
|
||||
# attributes we did not modify) is returned in an "Attributes" attribute:
|
||||
p = random_string()
|
||||
@@ -183,15 +159,6 @@ def test_update_item_returnvalues_all_old(test_table_s):
|
||||
ExpressionAttributeValues={':val': 'cat'})
|
||||
assert ret['Attributes'] == {'p': p, 'a': 'hi', 'b': 'dog'}
|
||||
|
||||
# If the item does not previously exist, "Attributes" is not returned
|
||||
# at all:
|
||||
p = random_string()
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='ALL_OLD',
|
||||
UpdateExpression='SET b = :val',
|
||||
ExpressionAttributeValues={':val': 'cat'})
|
||||
assert not 'Attributes' in ret
|
||||
|
||||
def test_update_item_returnvalues_updated_old(test_table_s):
|
||||
# With ReturnValues=UPDATED_OLD, only the overwritten attributes of the
|
||||
# old item are returned in an "Attributes" attribute:
|
||||
p = random_string()
|
||||
@@ -200,7 +167,6 @@ def test_update_item_returnvalues_updated_old(test_table_s):
|
||||
UpdateExpression='SET b = :val, c = :val2',
|
||||
ExpressionAttributeValues={':val': 'cat', ':val2': 'hello'})
|
||||
assert ret['Attributes'] == {'b': 'dog'}
|
||||
|
||||
# Even if an update overwrites an attribute by the same value again,
|
||||
# this is considered an update, and the old value (identical to the
|
||||
# new one) is returned:
|
||||
@@ -208,40 +174,11 @@ def test_update_item_returnvalues_updated_old(test_table_s):
|
||||
UpdateExpression='SET b = :val',
|
||||
ExpressionAttributeValues={':val': 'cat'})
|
||||
assert ret['Attributes'] == {'b': 'cat'}
|
||||
|
||||
# Deleting an attribute also counts as overwriting it, of course:
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='UPDATED_OLD',
|
||||
UpdateExpression='REMOVE b')
|
||||
assert ret['Attributes'] == {'b': 'cat'}
|
||||
|
||||
# If we write to an attribute that didn't exist before, we do not
|
||||
# get Attributes at all:
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='UPDATED_OLD',
|
||||
UpdateExpression='SET b = :val',
|
||||
ExpressionAttributeValues={':val': 'cat'})
|
||||
assert not 'Attributes' in ret
|
||||
|
||||
# However, if we write to two attributes, one which previously existed
|
||||
# and one didn't, we get back only the one which previously existed:
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='UPDATED_OLD',
|
||||
UpdateExpression='SET b = :val, d = :val2',
|
||||
ExpressionAttributeValues={':val': 'dog', ':val2': 'lion'})
|
||||
assert ret['Attributes'] == {'b': 'cat'}
|
||||
|
||||
# Of course if we write to two attributes which previously existed,
|
||||
# we get both of them back
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='UPDATED_OLD',
|
||||
UpdateExpression='SET b = :val, d = :val2',
|
||||
ExpressionAttributeValues={':val': 'cat', ':val2': 'tiger'})
|
||||
assert ret['Attributes'] == {'b': 'dog', 'd': 'lion'}
|
||||
|
||||
# If we write absolutely nothing (the only way to do this is with the
|
||||
# old AttributeUpdates syntax), we don't get Attributes back.
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='UPDATED_OLD',
|
||||
AttributeUpdates={})
|
||||
assert not 'Attributes' in ret
|
||||
|
||||
def test_update_item_returnvalues_all_new(test_table_s):
|
||||
# With ReturnValues=ALL_NEW, the entire new value of the item (including
|
||||
# old attributes we did not modify) is returned:
|
||||
p = random_string()
|
||||
@@ -251,67 +188,21 @@ def test_update_item_returnvalues_all_new(test_table_s):
|
||||
ExpressionAttributeValues={':val': 'cat'})
|
||||
assert ret['Attributes'] == {'p': p, 'a': 'hi', 'b': 'cat'}
|
||||
|
||||
# Verify that if a column is deleted, it is *not* returned:
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='ALL_NEW',
|
||||
UpdateExpression='REMOVE b')
|
||||
assert ret['Attributes'] == {'p': p, 'a': 'hi'}
|
||||
|
||||
# If the item did not previously exist, that's still fine, we get the
|
||||
# new value of the item:
|
||||
p = random_string()
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='ALL_NEW',
|
||||
UpdateExpression='SET b = :val',
|
||||
ExpressionAttributeValues={':val': 'cat'})
|
||||
assert ret['Attributes'] == {'p': p, 'b': 'cat'}
|
||||
|
||||
# A more interesting question is what happens if the item did not
|
||||
# previously exist, and all the update does is *delete* an attribute,
|
||||
# which never existed. In this case, the item is not created (see also
|
||||
# test_update_item_non_existent()), and "Attributes" is not returned,
|
||||
p = random_string()
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='ALL_NEW',
|
||||
UpdateExpression='REMOVE b')
|
||||
assert not 'Attributes' in ret
|
||||
ret=test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
assert not 'Item' in ret
|
||||
|
||||
# If we write absolutely nothing (the only way to do this is with the
|
||||
# old AttributeUpdates syntax), we get an empty item (just the key)
|
||||
# if it didn't yet exist (and now it does), or the old item if it did.
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='ALL_NEW',
|
||||
AttributeUpdates={})
|
||||
assert ret['Attributes'] == {'p': p}
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hi'})
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='ALL_NEW',
|
||||
AttributeUpdates={})
|
||||
assert ret['Attributes'] == {'p': p, 'a': 'hi'}
|
||||
|
||||
def test_update_item_returnvalues_updated_new(test_table_s):
|
||||
# With ReturnValues=UPDATED_NEW, only the new value of the updated
|
||||
# attributes are returned. Note that "updated attributes" means
|
||||
# the newly set attributes - it doesn't require that these attributes
|
||||
# have any previous values.
|
||||
# have any previous values
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hi', 'b': 'dog'})
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='UPDATED_NEW',
|
||||
UpdateExpression='SET b = :val, c = :val2',
|
||||
ExpressionAttributeValues={':val': 'cat', ':val2': 'hello'})
|
||||
assert ret['Attributes'] == {'b': 'cat', 'c': 'hello'}
|
||||
|
||||
# Deleting an attribute also counts as overwriting it, but the deleted
|
||||
# Deleting an attribute also counts as overwriting it, but the delete
|
||||
# column is not returned in the response - so it's empty in this case.
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='UPDATED_NEW',
|
||||
UpdateExpression='REMOVE b')
|
||||
assert not 'Attributes' in ret
|
||||
|
||||
# Verify If we write to multiple attributes, we get them all back,
|
||||
# regardless of whether they previously existed or not (and again,
|
||||
# deleted columns aren't returned):
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='UPDATED_NEW',
|
||||
UpdateExpression='SET a = :val, b = :val2 REMOVE c',
|
||||
ExpressionAttributeValues={':val': 'dog', ':val2': 'tiger'})
|
||||
assert ret['Attributes'] == {'a': 'dog', 'b': 'tiger'}
|
||||
|
||||
# In the above examples, UPDATED_NEW is not useful because it just
|
||||
# returns the new values we already know from the request... UPDATED_NEW
|
||||
# becomes more useful in read-modify-write operations:
|
||||
@@ -322,79 +213,14 @@ def test_update_item_returnvalues_updated_new(test_table_s):
|
||||
ExpressionAttributeValues={':val': 1})
|
||||
assert ret['Attributes'] == {'a': 2}
|
||||
|
||||
# UPDATED_NEW only returns non-key attributes. Even if the operation
|
||||
# caused a new item to be created, the new key attributes aren't
|
||||
# returned.
|
||||
p = random_string()
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='UPDATED_NEW',
|
||||
UpdateExpression='SET a = :val',
|
||||
ExpressionAttributeValues={':val': 1})
|
||||
assert ret['Attributes'] == {'a': 1}
|
||||
|
||||
# If we write absolutely nothing (the only way to do this is with the
|
||||
# old AttributeUpdates syntax), we don't get back any Attributes.
|
||||
# Not even if the item didn't previously exist and this write caused
|
||||
# it to be created.
|
||||
p = random_string()
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='UPDATED_NEW',
|
||||
AttributeUpdates={})
|
||||
assert not 'Attributes' in ret
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hi'})
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='UPDATED_NEW',
|
||||
AttributeUpdates={})
|
||||
assert not 'Attributes' in ret
|
||||
|
||||
# Test the ReturnValues from an UpdateItem directly modifying a *nested*
|
||||
# attribute, in the relevant ReturnValue modes:
|
||||
@pytest.mark.xfail(reason="nested updates not yet implemented")
|
||||
def test_update_item_returnvalues_nested(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': {'b': 'dog', 'c': [1, 2, 3]}, 'd': 'cat'})
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='ALL_OLD',
|
||||
UpdateExpression='SET a.b = :val',
|
||||
ExpressionAttributeValues={':val': 'hi'})
|
||||
assert ret['Attributes'] == {'p': p, 'a': {'b': 'dog', 'c': [1, 2, 3]}, 'd': 'cat'}
|
||||
# UPDATED_OLD and UPDATED_NEW return *only* the nested attribute, not
|
||||
# the entire top-level attribute. It still needs to return it nested
|
||||
# within its the appropriate hierarchy, but not containing the entire
|
||||
# top-level attribute.
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='UPDATED_OLD',
|
||||
UpdateExpression='SET a.b = :val',
|
||||
ExpressionAttributeValues={':val': 'yo'})
|
||||
assert ret['Attributes'] == {'a': {'b': 'hi'}}
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='UPDATED_OLD',
|
||||
UpdateExpression='SET a.c[1] = :val',
|
||||
ExpressionAttributeValues={':val': 7})
|
||||
assert ret['Attributes'] == {'a': {'c': [2]}}
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='UPDATED_OLD',
|
||||
UpdateExpression='SET a.c[1] = :val, a.b = :val2',
|
||||
ExpressionAttributeValues={':val': 8, ':val2': 'dog'})
|
||||
assert ret['Attributes'] == {'a': {'b': 'yo', 'c': [7]}}
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='UPDATED_NEW',
|
||||
UpdateExpression='SET a.b = :val',
|
||||
ExpressionAttributeValues={':val': 'hello'})
|
||||
assert ret['Attributes'] == {'a': {'b': 'hello'}}
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='UPDATED_NEW',
|
||||
UpdateExpression='SET a.c[1] = :val',
|
||||
ExpressionAttributeValues={':val': 4})
|
||||
assert ret['Attributes'] == {'a': {'c': [4]}}
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='UPDATED_NEW',
|
||||
UpdateExpression='SET a.c[1] = :val, a.b = :val2',
|
||||
ExpressionAttributeValues={':val': 3, ':val2': 'dog'})
|
||||
assert ret['Attributes'] == {'a': {'b': 'dog', 'c': [3]}}
|
||||
# Although ALL_NEW mode returns the entire item and doesn't need to
|
||||
# know how to project a nested attribute, we still need to check that
|
||||
# it can return the post-update value of the attribute within the entire
|
||||
# item.
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='ALL_NEW',
|
||||
UpdateExpression='SET a.b = :val',
|
||||
ExpressionAttributeValues={':val': 'hi'})
|
||||
assert ret['Attributes'] == {'p': p, 'a': {'b': 'hi', 'c': [1, 3, 3]}, 'd': 'cat' }
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='ALL_NEW',
|
||||
UpdateExpression='SET a.c[1] = :val',
|
||||
ExpressionAttributeValues={':val': 4})
|
||||
assert ret['Attributes'] == {'p': p, 'a': {'b': 'hi', 'c': [1, 4, 3]}, 'd': 'cat' }
|
||||
ret=test_table_s.update_item(Key={'p': p}, ReturnValues='ALL_NEW',
|
||||
UpdateExpression='SET a.c[1] = :val, a.b = :val2',
|
||||
ExpressionAttributeValues={':val': 3, ':val2': 'dog'})
|
||||
assert ret['Attributes'] == {'p': p, 'a': {'b': 'dog', 'c': [1, 3, 3]}, 'd': 'cat' }
|
||||
# A non-supported setting "DOG" also returns in error:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, ReturnValues='DOG',
|
||||
UpdateExpression='SET a = a + :val',
|
||||
ExpressionAttributeValues={':val': 1})
|
||||
# The ReturnValues value is case sensitive, so while "NONE" is supported
|
||||
# (and tested above), "none" isn't:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, ReturnValues='none',
|
||||
UpdateExpression='SET a = a + :val',
|
||||
ExpressionAttributeValues={':val': 1})
|
||||
@@ -30,10 +30,10 @@ def test_scan_basic(filled_test_table):
|
||||
got_items = []
|
||||
while True:
|
||||
if limit:
|
||||
response = test_table.scan(Limit=limit, ConsistentRead=True, ExclusiveStartKey=pos) if pos else test_table.scan(Limit=limit, ConsistentRead=True)
|
||||
response = test_table.scan(Limit=limit, ExclusiveStartKey=pos) if pos else test_table.scan(Limit=limit)
|
||||
assert len(response['Items']) <= limit
|
||||
else:
|
||||
response = test_table.scan(ExclusiveStartKey=pos, ConsistentRead=True) if pos else test_table.scan(ConsistentRead=True)
|
||||
response = test_table.scan(ExclusiveStartKey=pos) if pos else test_table.scan()
|
||||
pos = response.get('LastEvaluatedKey', None)
|
||||
got_items += response['Items']
|
||||
if not pos:
|
||||
@@ -42,11 +42,6 @@ def test_scan_basic(filled_test_table):
|
||||
assert len(items) == len(got_items)
|
||||
assert multiset(items) == multiset(got_items)
|
||||
|
||||
def test_scan_nonexistent_table(dynamodb):
|
||||
client = dynamodb.meta.client
|
||||
with pytest.raises(ClientError, match="ResourceNotFoundException"):
|
||||
client.scan(TableName="i_do_not_exist")
|
||||
|
||||
def test_scan_with_paginator(dynamodb, filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
paginator = dynamodb.meta.client.get_paginator('scan')
|
||||
@@ -128,6 +123,7 @@ def test_scan_with_attribute_equality_filtering(dynamodb, filled_test_table):
|
||||
assert multiset(expected_items) == multiset(got_items)
|
||||
|
||||
# Test that FilterExpression works as expected
|
||||
@pytest.mark.xfail(reason="FilterExpression not supported yet")
|
||||
def test_scan_filter_expression(filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
|
||||
@@ -243,6 +239,7 @@ def test_scan_select(filled_test_table):
|
||||
# a scan into multiple parts, and that these parts are in fact disjoint,
|
||||
# and their union is the entire contents of the table. We do not actually
|
||||
# try to run these queries in *parallel* in this test.
|
||||
@pytest.mark.xfail(reason="parallel scan not supported yet")
|
||||
def test_scan_parallel(filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
for nsegments in [1, 2, 17]:
|
||||
@@ -253,14 +250,3 @@ def test_scan_parallel(filled_test_table):
|
||||
# The following comparison verifies that each of the expected item
|
||||
# in items was returned in one - and just one - of the segments.
|
||||
assert multiset(items) == multiset(got_items)
|
||||
|
||||
# Test correct handling of incorrect parallel scan parameters.
|
||||
# Most of the corner cases (like TotalSegments=0) are validated
|
||||
# by boto3 itself, but some checks can still be performed.
|
||||
def test_scan_parallel_incorrect(filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
with pytest.raises(ClientError, match='ValidationException.*Segment'):
|
||||
full_scan(test_table, TotalSegments=1000001, Segment=0)
|
||||
for segment in [7, 9]:
|
||||
with pytest.raises(ClientError, match='ValidationException.*Segment'):
|
||||
full_scan(test_table, TotalSegments=5, Segment=segment)
|
||||
@@ -74,11 +74,6 @@ def create_and_delete_table(dynamodb, name, **kwargs):
|
||||
def test_create_and_delete_table(dynamodb):
|
||||
create_and_delete_table(dynamodb, 'alternator_test')
|
||||
|
||||
# Test that recreating a table right after deleting it works without issues
|
||||
def test_recreate_table(dynamodb):
|
||||
create_and_delete_table(dynamodb, 'alternator_recr_test')
|
||||
create_and_delete_table(dynamodb, 'alternator_recr_test')
|
||||
|
||||
# DynamoDB documentation specifies that table names must be 3-255 characters,
|
||||
# and match the regex [a-zA-Z0-9._-]+. Names not matching these rules should
|
||||
# be rejected, and no table be created.
|
||||
@@ -200,7 +195,7 @@ def test_create_table_invalid_schema(dynamodb):
|
||||
# Test that trying to create a table that already exists fails in the
|
||||
# appropriate way (ResourceInUseException)
|
||||
def test_create_table_already_exists(dynamodb, test_table):
|
||||
with pytest.raises(ClientError, match='ResourceInUseException.*Table.*already exists'):
|
||||
with pytest.raises(ClientError, match='ResourceInUseException'):
|
||||
create_table(dynamodb, test_table.name)
|
||||
|
||||
# Test that BillingMode error path works as expected - only the values
|
||||
@@ -232,35 +227,6 @@ def test_create_table_billing_mode_errors(dynamodb, test_table):
|
||||
KeySchema=[{ 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }])
|
||||
|
||||
# Even before Alternator gains full support for the DynamoDB stream API
|
||||
# and CreateTable's StreamSpecification option, we should support the
|
||||
# options which mean it is turned *off*.
|
||||
def test_table_streams_off(dynamodb):
|
||||
# If StreamSpecification is given, but has StreamEnabled=false, it's as
|
||||
# if StreamSpecification was missing. StreamViewType isn't needed.
|
||||
table = create_test_table(dynamodb, StreamSpecification={'StreamEnabled': False},
|
||||
KeySchema=[{ 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }]);
|
||||
table.delete();
|
||||
# DynamoDB doesn't allow StreamSpecification to be empty map - if it
|
||||
# exists, it must have a StreamEnabled
|
||||
# Unfortunately, new versions of boto3 doesn't let us pass this...
|
||||
#with pytest.raises(ClientError, match='ValidationException'):
|
||||
# table = create_test_table(dynamodb, StreamSpecification={},
|
||||
# KeySchema=[{ 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
# AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }]);
|
||||
# table.delete();
|
||||
# Unfortunately, boto3 doesn't allow us to pass StreamSpecification=None.
|
||||
# This is what we had in issue #5796.
|
||||
|
||||
def test_table_streams_on(dynamodb):
|
||||
for type in [ 'OLD_IMAGE', 'NEW_IMAGE', 'KEYS_ONLY', 'NEW_AND_OLD_IMAGES']:
|
||||
table = create_test_table(dynamodb,
|
||||
StreamSpecification={'StreamEnabled': True, 'StreamViewType': type},
|
||||
KeySchema=[{ 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }]);
|
||||
table.delete();
|
||||
|
||||
# Our first implementation had a special column name called "attrs" where
|
||||
# we stored a map for all non-key columns. If the user tried to name one
|
||||
# of the key columns with this same name, the result was a disaster - Scylla
|
||||
@@ -308,17 +274,3 @@ def test_list_tables_wrong_limit(dynamodb):
|
||||
# lower limit (min. 1) is imposed by boto3 library checks
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
dynamodb.meta.client.list_tables(Limit=101)
|
||||
|
||||
# Even before Alternator gains support for configuring server-side encryption
|
||||
# ("encryption at rest") with CreateTable's SSESpecification option, we should
|
||||
# support the option "Enabled=false" which is the default, and means the server
|
||||
# takes care of whatever server-side encryption is done, on its own.
|
||||
# Reproduces issue #7031.
|
||||
def test_table_sse_off(dynamodb):
|
||||
# If StreamSpecification is given, but has StreamEnabled=false, it's as
|
||||
# if StreamSpecification was missing, and fine. No other attribues are
|
||||
# necessary.
|
||||
table = create_test_table(dynamodb, SSESpecification = {'Enabled': False},
|
||||
KeySchema=[{ 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }]);
|
||||
table.delete();
|
||||
@@ -21,6 +21,7 @@ import random
|
||||
import string
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
from decimal import Decimal
|
||||
from util import random_string
|
||||
|
||||
# The simplest test of using UpdateExpression to set a top-level attribute,
|
||||
@@ -369,7 +370,7 @@ def test_update_expression_cannot_modify_key(test_table):
|
||||
|
||||
# Test that trying to start an expression with some nonsense like HELLO
|
||||
# instead of SET, REMOVE, ADD or DELETE, fails.
|
||||
def test_update_expression_non_existent_clause(test_table_s):
|
||||
def test_update_expression_non_existant_clause(test_table_s):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
@@ -406,6 +407,21 @@ def test_update_expression_plus_basic(test_table_s):
|
||||
UpdateExpression='SET b = :val1 + :val2',
|
||||
ExpressionAttributeValues={':val1': ['a', 'b'], ':val2': ['1', '2']})
|
||||
|
||||
# While most of the Alternator code just saves high-precision numbers
|
||||
# unchanged, the "+" and "-" operations need to calculate with them, and
|
||||
# we should check the calculation isn't done with some lower-precision
|
||||
# representation, e.g., double
|
||||
def test_update_expression_plus_precision(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = :val1 + :val2',
|
||||
ExpressionAttributeValues={':val1': Decimal("1"), ':val2': Decimal("10000000000000000000000")})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': Decimal("10000000000000000000001")}
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = :val2 - :val1',
|
||||
ExpressionAttributeValues={':val1': Decimal("1"), ':val2': Decimal("10000000000000000000000")})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': Decimal("9999999999999999999999")}
|
||||
|
||||
# Test support for "SET a = b + :val2" et al., i.e., a version of the
|
||||
# above test_update_expression_plus_basic with read before write.
|
||||
def test_update_expression_plus_rmw(test_table_s):
|
||||
@@ -701,28 +717,10 @@ def test_update_expression_delete_sets(test_table_s):
|
||||
UpdateExpression='DELETE a :val1',
|
||||
ExpressionAttributeValues={':val1': set(['pig'])})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == set(['dog'])
|
||||
# Deleting all the elements cannot leave an empty set (which isn't
|
||||
# supported). Rather, it deletes the attribute altogether:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='DELETE a :val1',
|
||||
ExpressionAttributeValues={':val1': set(['dog'])})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 'hi'}
|
||||
# Deleting elements from a non-existent attribute is allowed, and
|
||||
# simply does nothing:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='DELETE a :val1',
|
||||
ExpressionAttributeValues={':val1': set(['dog'])})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 'hi'}
|
||||
# An empty set parameter is not allowed
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='DELETE a :val1',
|
||||
ExpressionAttributeValues={':val1': set([])})
|
||||
# The value to be deleted must be a set of the same type - it can't
|
||||
# be a single element or anything else. If the value has the wrong type,
|
||||
# we get an error like "Invalid UpdateExpression: Incorrect operand type
|
||||
# for operator or function; operator: DELETE, operand type: STRING".
|
||||
test_table_s.put_item(Item={'p': p, 'a': set(['dog', 'cat', 'mouse']), 'b': 'hi'})
|
||||
with pytest.raises(ClientError, match='ValidationException.*type'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='DELETE a :val1',
|
||||
@@ -854,24 +852,3 @@ def test_nested_attribute_update_bad_path_array(test_table_s):
|
||||
with pytest.raises(ClientError, match='ValidationException.*path'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a[0] = :val1',
|
||||
ExpressionAttributeValues={':val1': 7})
|
||||
|
||||
# DynamoDB Does not allow empty sets.
|
||||
# Trying to ask UpdateItem to put one of these in an attribute should be
|
||||
# forbidden. Empty lists and maps *are* allowed.
|
||||
# Note that in test_item.py::test_update_item_empty_attribute we checked
|
||||
# this with the AttributeUpdates syntax. Here we check the same with the
|
||||
# UpdateExpression syntax.
|
||||
def test_update_expression_empty_attribute(test_table_s):
|
||||
p = random_string()
|
||||
# Empty sets are *not* allowed
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = :v',
|
||||
ExpressionAttributeValues={':v': set()})
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
# But empty lists, maps, strings and binary blobs *are* allowed:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET d = :v1, e = :v2, f = :v3, g = :v4',
|
||||
ExpressionAttributeValues={':v1': [], ':v2': {}, ':v3': '', ':v4': b''})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'd': [], 'e': {}, 'f': '', 'g': b''}
|
||||
#
|
||||
@@ -28,18 +28,14 @@ def random_string(length=10, chars=string.ascii_uppercase + string.digits):
|
||||
def random_bytes(length=10):
|
||||
return bytearray(random.getrandbits(8) for _ in range(length))
|
||||
|
||||
# Utility functions for scan and query into an array of items, reading
|
||||
# the full (possibly requiring multiple requests to read successive pages).
|
||||
# For convenience, ConsistentRead=True is used by default, as most tests
|
||||
# need it to run correctly on a multi-node cluster. Callers who need to
|
||||
# override it, can (this is necessary in GSI tests, where ConsistentRead=True
|
||||
# is not supported).
|
||||
def full_scan(table, ConsistentRead=True, **kwargs):
|
||||
response = table.scan(ConsistentRead=ConsistentRead, **kwargs)
|
||||
# Utility functions for scan and query into an array of items:
|
||||
# TODO: add to full_scan and full_query by default ConsistentRead=True, as
|
||||
# it's not useful for tests without it!
|
||||
def full_scan(table, **kwargs):
|
||||
response = table.scan(**kwargs)
|
||||
items = response['Items']
|
||||
while 'LastEvaluatedKey' in response:
|
||||
response = table.scan(ExclusiveStartKey=response['LastEvaluatedKey'],
|
||||
ConsistentRead=ConsistentRead, **kwargs)
|
||||
response = table.scan(ExclusiveStartKey=response['LastEvaluatedKey'], **kwargs)
|
||||
items.extend(response['Items'])
|
||||
return items
|
||||
|
||||
@@ -47,8 +43,8 @@ def full_scan(table, ConsistentRead=True, **kwargs):
|
||||
# Note that count isn't simply len(items) - the server returns them
|
||||
# independently. e.g., with Select='COUNT' the items are not returned, but
|
||||
# count is.
|
||||
def full_scan_and_count(table, ConsistentRead=True, **kwargs):
|
||||
response = table.scan(ConsistentRead=ConsistentRead, **kwargs)
|
||||
def full_scan_and_count(table, **kwargs):
|
||||
response = table.scan(**kwargs)
|
||||
items = []
|
||||
count = 0
|
||||
if 'Items' in response:
|
||||
@@ -56,8 +52,7 @@ def full_scan_and_count(table, ConsistentRead=True, **kwargs):
|
||||
if 'Count' in response:
|
||||
count = count + response['Count']
|
||||
while 'LastEvaluatedKey' in response:
|
||||
response = table.scan(ExclusiveStartKey=response['LastEvaluatedKey'],
|
||||
ConsistentRead=ConsistentRead, **kwargs)
|
||||
response = table.scan(ExclusiveStartKey=response['LastEvaluatedKey'], **kwargs)
|
||||
if 'Items' in response:
|
||||
items.extend(response['Items'])
|
||||
if 'Count' in response:
|
||||
@@ -65,45 +60,14 @@ def full_scan_and_count(table, ConsistentRead=True, **kwargs):
|
||||
return (count, items)
|
||||
|
||||
# Utility function for fetching the entire results of a query into an array of items
|
||||
def full_query(table, ConsistentRead=True, **kwargs):
|
||||
response = table.query(ConsistentRead=ConsistentRead, **kwargs)
|
||||
def full_query(table, **kwargs):
|
||||
response = table.query(**kwargs)
|
||||
items = response['Items']
|
||||
while 'LastEvaluatedKey' in response:
|
||||
response = table.query(ExclusiveStartKey=response['LastEvaluatedKey'],
|
||||
ConsistentRead=ConsistentRead, **kwargs)
|
||||
response = table.query(ExclusiveStartKey=response['LastEvaluatedKey'], **kwargs)
|
||||
items.extend(response['Items'])
|
||||
return items
|
||||
|
||||
# full_query_and_counts returns both items and counts (pre-filter and
|
||||
# post-filter count) as returned by the server.
|
||||
# Note that count isn't simply len(items) - the server returns them
|
||||
# independently. e.g., with Select='COUNT' the items are not returned, but
|
||||
# count is.
|
||||
def full_query_and_counts(table, ConsistentRead=True, **kwargs):
|
||||
response = table.query(ConsistentRead=ConsistentRead, **kwargs)
|
||||
items = []
|
||||
prefilter_count = 0
|
||||
postfilter_count = 0
|
||||
pages = 0
|
||||
if 'Items' in response:
|
||||
items.extend(response['Items'])
|
||||
pages = pages + 1
|
||||
if 'Count' in response:
|
||||
postfilter_count = postfilter_count + response['Count']
|
||||
if 'ScannedCount' in response:
|
||||
prefilter_count = prefilter_count + response['ScannedCount']
|
||||
while 'LastEvaluatedKey' in response:
|
||||
response = table.query(ExclusiveStartKey=response['LastEvaluatedKey'],
|
||||
ConsistentRead=ConsistentRead, **kwargs)
|
||||
if 'Items' in response:
|
||||
items.extend(response['Items'])
|
||||
pages = pages + 1
|
||||
if 'Count' in response:
|
||||
postfilter_count = postfilter_count + response['Count']
|
||||
if 'ScannedCount' in response:
|
||||
prefilter_count = prefilter_count + response['ScannedCount']
|
||||
return (prefilter_count, postfilter_count, pages, items)
|
||||
|
||||
# To compare two lists of items (each is a dict) without regard for order,
|
||||
# "==" is not good enough because it will fail if the order is different.
|
||||
# The following function, multiset() converts the list into a multiset
|
||||
@@ -120,9 +84,8 @@ def freeze(item):
|
||||
def multiset(items):
|
||||
return collections.Counter([freeze(item) for item in items])
|
||||
|
||||
# NOTE: alternator_Test prefix contains a capital letter on purpose,
|
||||
#in order to validate case sensitivity in alternator
|
||||
test_table_prefix = 'alternator_Test_'
|
||||
|
||||
test_table_prefix = 'alternator_test_'
|
||||
def test_table_name():
|
||||
current_ms = int(round(time.time() * 1000))
|
||||
# In the off chance that test_table_name() is called twice in the same millisecond...
|
||||
@@ -78,12 +78,12 @@ void check_expiry(std::string_view signature_date) {
|
||||
std::string expiration_str = format_time_point(db_clock::now() - 15min);
|
||||
std::string validity_str = format_time_point(db_clock::now() + 15min);
|
||||
if (signature_date < expiration_str) {
|
||||
throw api_error::invalid_signature(
|
||||
throw api_error("InvalidSignatureException",
|
||||
fmt::format("Signature expired: {} is now earlier than {} (current time - 15 min.)",
|
||||
signature_date, expiration_str));
|
||||
}
|
||||
if (signature_date > validity_str) {
|
||||
throw api_error::invalid_signature(
|
||||
throw api_error("InvalidSignatureException",
|
||||
fmt::format("Signature not yet current: {} is still later than {} (current time + 15 min.)",
|
||||
signature_date, validity_str));
|
||||
}
|
||||
@@ -94,13 +94,13 @@ std::string get_signature(std::string_view access_key_id, std::string_view secre
|
||||
std::string_view body_content, std::string_view region, std::string_view service, std::string_view query_string) {
|
||||
auto amz_date_it = signed_headers_map.find("x-amz-date");
|
||||
if (amz_date_it == signed_headers_map.end()) {
|
||||
throw api_error::invalid_signature("X-Amz-Date header is mandatory for signature verification");
|
||||
throw api_error("InvalidSignatureException", "X-Amz-Date header is mandatory for signature verification");
|
||||
}
|
||||
std::string_view amz_date = amz_date_it->second;
|
||||
check_expiry(amz_date);
|
||||
std::string_view datestamp = amz_date.substr(0, 8);
|
||||
if (datestamp != orig_datestamp) {
|
||||
throw api_error::invalid_signature(
|
||||
throw api_error("InvalidSignatureException",
|
||||
format("X-Amz-Date date does not match the provided datestamp. Expected {}, got {}",
|
||||
orig_datestamp, datestamp));
|
||||
}
|
||||
@@ -126,18 +126,19 @@ std::string get_signature(std::string_view access_key_id, std::string_view secre
|
||||
|
||||
future<std::string> get_key_from_roles(cql3::query_processor& qp, std::string username) {
|
||||
static const sstring query = format("SELECT salted_hash FROM {} WHERE {} = ?",
|
||||
auth::meta::roles_table::qualified_name, auth::meta::roles_table::role_col_name);
|
||||
auth::meta::roles_table::qualified_name(), auth::meta::roles_table::role_col_name);
|
||||
|
||||
auto cl = auth::password_authenticator::consistency_for_user(username);
|
||||
return qp.execute_internal(query, cl, auth::internal_distributed_query_state(), {sstring(username)}, true).then_wrapped([username = std::move(username)] (future<::shared_ptr<cql3::untyped_result_set>> f) {
|
||||
auto timeout = auth::internal_distributed_timeout_config();
|
||||
return qp.process(query, cl, timeout, {sstring(username)}, true).then_wrapped([username = std::move(username)] (future<::shared_ptr<cql3::untyped_result_set>> f) {
|
||||
auto res = f.get0();
|
||||
auto salted_hash = std::optional<sstring>();
|
||||
if (res->empty()) {
|
||||
throw api_error::unrecognized_client(fmt::format("User not found: {}", username));
|
||||
throw api_error("UnrecognizedClientException", fmt::format("User not found: {}", username));
|
||||
}
|
||||
salted_hash = res->one().get_opt<sstring>("salted_hash");
|
||||
if (!salted_hash) {
|
||||
throw api_error::unrecognized_client(fmt::format("No password found for user: {}", username));
|
||||
throw api_error("UnrecognizedClientException", fmt::format("No password found for user: {}", username));
|
||||
}
|
||||
return make_ready_future<std::string>(*salted_hash);
|
||||
});
|
||||
|
||||
@@ -32,13 +32,13 @@
|
||||
// and the character used in base64 encoding to represent it.
|
||||
static class base64_chars {
|
||||
public:
|
||||
static constexpr const char to[] =
|
||||
static constexpr const char* to =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
int8_t from[255];
|
||||
base64_chars() {
|
||||
static_assert(sizeof(to) == 64 + 1);
|
||||
static_assert(strlen(to) == 64);
|
||||
for (int i = 0; i < 255; i++) {
|
||||
from[i] = -1; // signal invalid character
|
||||
from[i] = 255; // signal invalid character
|
||||
}
|
||||
for (int i = 0; i < 64; i++) {
|
||||
from[(unsigned) to[i]] = i;
|
||||
@@ -77,7 +77,7 @@ std::string base64_encode(bytes_view in) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
static std::string base64_decode_string(std::string_view in) {
|
||||
bytes base64_decode(std::string_view in) {
|
||||
int i = 0;
|
||||
int8_t chunk4[4]; // chunk of input, each byte converted to 0..63;
|
||||
std::string ret;
|
||||
@@ -104,42 +104,8 @@ static std::string base64_decode_string(std::string_view in) {
|
||||
if (i==3)
|
||||
ret += ((chunk4[1] & 0xf) << 4) + ((chunk4[2] & 0x3c) >> 2);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bytes base64_decode(std::string_view in) {
|
||||
// FIXME: This copy is sad. The problem is we need back "bytes"
|
||||
// but "bytes" doesn't have efficient append and std::string.
|
||||
// To fix this we need to use bytes' "uninitialized" feature.
|
||||
std::string ret = base64_decode_string(in);
|
||||
return bytes(ret.begin(), ret.end());
|
||||
}
|
||||
|
||||
static size_t base64_padding_len(std::string_view str) {
|
||||
size_t padding = 0;
|
||||
padding += (!str.empty() && str.back() == '=');
|
||||
padding += (str.size() > 1 && *(str.end() - 2) == '=');
|
||||
return padding;
|
||||
}
|
||||
|
||||
size_t base64_decoded_len(std::string_view str) {
|
||||
return str.size() / 4 * 3 - base64_padding_len(str);
|
||||
}
|
||||
|
||||
bool base64_begins_with(std::string_view base, std::string_view operand) {
|
||||
if (base.size() < operand.size() || base.size() % 4 != 0 || operand.size() % 4 != 0) {
|
||||
return false;
|
||||
}
|
||||
if (base64_padding_len(operand) == 0) {
|
||||
return base.starts_with(operand);
|
||||
}
|
||||
const std::string_view unpadded_base_prefix = base.substr(0, operand.size() - 4);
|
||||
const std::string_view unpadded_operand = operand.substr(0, operand.size() - 4);
|
||||
if (unpadded_base_prefix != unpadded_operand) {
|
||||
return false;
|
||||
}
|
||||
// Decode and compare last 4 bytes of base64-encoded strings
|
||||
const std::string base_remainder = base64_decode_string(base.substr(operand.size() - 4, operand.size()));
|
||||
const std::string operand_remainder = base64_decode_string(operand.substr(operand.size() - 4));
|
||||
return base_remainder.starts_with(operand_remainder);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
#include <string_view>
|
||||
#include "bytes.hh"
|
||||
#include "utils/rjson.hh"
|
||||
#include "rjson.hh"
|
||||
|
||||
std::string base64_encode(bytes_view);
|
||||
|
||||
@@ -32,7 +32,3 @@ bytes base64_decode(std::string_view);
|
||||
inline bytes base64_decode(const rjson::value& v) {
|
||||
return base64_decode(std::string_view(v.GetString(), v.GetStringLength()));
|
||||
}
|
||||
|
||||
size_t base64_decoded_len(std::string_view str);
|
||||
|
||||
bool base64_begins_with(std::string_view base, std::string_view operand);
|
||||
|
||||
@@ -26,15 +26,10 @@
|
||||
#include "alternator/error.hh"
|
||||
#include "cql3/constants.hh"
|
||||
#include <unordered_map>
|
||||
#include "utils/rjson.hh"
|
||||
#include "rjson.hh"
|
||||
#include "serialization.hh"
|
||||
#include "base64.hh"
|
||||
#include <stdexcept>
|
||||
#include <boost/algorithm/cxx11/all_of.hpp>
|
||||
#include <boost/algorithm/cxx11/any_of.hpp>
|
||||
#include "utils/overloaded_functor.hh"
|
||||
|
||||
#include "expressions.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
@@ -57,16 +52,59 @@ comparison_operator_type get_comparison_operator(const rjson::value& comparison_
|
||||
{"NOT_CONTAINS", comparison_operator_type::NOT_CONTAINS},
|
||||
};
|
||||
if (!comparison_operator.IsString()) {
|
||||
throw api_error::validation(format("Invalid comparison operator definition {}", rjson::print(comparison_operator)));
|
||||
throw api_error("ValidationException", format("Invalid comparison operator definition {}", rjson::print(comparison_operator)));
|
||||
}
|
||||
std::string op = comparison_operator.GetString();
|
||||
auto it = ops.find(op);
|
||||
if (it == ops.end()) {
|
||||
throw api_error::validation(format("Unsupported comparison operator {}", op));
|
||||
throw api_error("ValidationException", format("Unsupported comparison operator {}", op));
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
static ::shared_ptr<cql3::restrictions::single_column_restriction::contains> make_map_element_restriction(const column_definition& cdef, std::string_view key, const rjson::value& value) {
|
||||
bytes raw_key = utf8_type->from_string(sstring_view(key.data(), key.size()));
|
||||
auto key_value = ::make_shared<cql3::constants::value>(cql3::raw_value::make_value(std::move(raw_key)));
|
||||
bytes raw_value = serialize_item(value);
|
||||
auto entry_value = ::make_shared<cql3::constants::value>(cql3::raw_value::make_value(std::move(raw_value)));
|
||||
return make_shared<cql3::restrictions::single_column_restriction::contains>(cdef, std::move(key_value), std::move(entry_value));
|
||||
}
|
||||
|
||||
static ::shared_ptr<cql3::restrictions::single_column_restriction::EQ> make_key_eq_restriction(const column_definition& cdef, const rjson::value& value) {
|
||||
bytes raw_value = get_key_from_typed_value(value, cdef, type_to_string(cdef.type));
|
||||
auto restriction_value = ::make_shared<cql3::constants::value>(cql3::raw_value::make_value(std::move(raw_value)));
|
||||
return make_shared<cql3::restrictions::single_column_restriction::EQ>(cdef, std::move(restriction_value));
|
||||
}
|
||||
|
||||
::shared_ptr<cql3::restrictions::statement_restrictions> get_filtering_restrictions(schema_ptr schema, const column_definition& attrs_col, const rjson::value& query_filter) {
|
||||
clogger.trace("Getting filtering restrictions for: {}", rjson::print(query_filter));
|
||||
auto filtering_restrictions = ::make_shared<cql3::restrictions::statement_restrictions>(schema, true);
|
||||
for (auto it = query_filter.MemberBegin(); it != query_filter.MemberEnd(); ++it) {
|
||||
std::string_view column_name(it->name.GetString(), it->name.GetStringLength());
|
||||
const rjson::value& condition = it->value;
|
||||
|
||||
const rjson::value& comp_definition = rjson::get(condition, "ComparisonOperator");
|
||||
const rjson::value& attr_list = rjson::get(condition, "AttributeValueList");
|
||||
comparison_operator_type op = get_comparison_operator(comp_definition);
|
||||
|
||||
if (op != comparison_operator_type::EQ) {
|
||||
throw api_error("ValidationException", "Filtering is currently implemented for EQ operator only");
|
||||
}
|
||||
if (attr_list.Size() != 1) {
|
||||
throw api_error("ValidationException", format("EQ restriction needs exactly 1 attribute value: {}", rjson::print(attr_list)));
|
||||
}
|
||||
if (const column_definition* cdef = schema->get_column_definition(to_bytes(column_name.data()))) {
|
||||
// Primary key restriction
|
||||
filtering_restrictions->add_restriction(make_key_eq_restriction(*cdef, attr_list[0]), false, true);
|
||||
} else {
|
||||
// Regular column restriction
|
||||
filtering_restrictions->add_restriction(make_map_element_restriction(attrs_col, column_name, attr_list[0]), false, true);
|
||||
}
|
||||
|
||||
}
|
||||
return filtering_restrictions;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct size_check {
|
||||
@@ -98,16 +136,11 @@ struct nonempty : public size_check {
|
||||
|
||||
// Check that array has the expected number of elements
|
||||
static void verify_operand_count(const rjson::value* array, const size_check& expected, const rjson::value& op) {
|
||||
if (!array && expected(0)) {
|
||||
// If expected() allows an empty AttributeValueList, it is also fine
|
||||
// that it is missing.
|
||||
return;
|
||||
}
|
||||
if (!array || !array->IsArray()) {
|
||||
throw api_error::validation("With ComparisonOperator, AttributeValueList must be given and an array");
|
||||
throw api_error("ValidationException", "With ComparisonOperator, AttributeValueList must be given and an array");
|
||||
}
|
||||
if (!expected(array->Size())) {
|
||||
throw api_error::validation(
|
||||
throw api_error("ValidationException",
|
||||
format("{} operator requires AttributeValueList {}, instead found list size {}",
|
||||
op, expected.what(), array->Size()));
|
||||
}
|
||||
@@ -131,7 +164,7 @@ static bool check_EQ_for_sets(const rjson::value& set1, const rjson::value& set2
|
||||
set1_raw.insert(&*it);
|
||||
}
|
||||
for (const auto& a : set2.GetArray()) {
|
||||
if (!set1_raw.contains(&a)) {
|
||||
if (set1_raw.count(&a) == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -164,11 +197,11 @@ static bool check_BEGINS_WITH(const rjson::value* v1, const rjson::value& v2) {
|
||||
// binary - otherwise it's a validation error. However, problems with
|
||||
// the stored attribute (v1) will just return false (no match).
|
||||
if (!v2.IsObject() || v2.MemberCount() != 1) {
|
||||
throw api_error::validation(format("BEGINS_WITH operator encountered malformed AttributeValue: {}", v2));
|
||||
throw api_error("ValidationException", format("BEGINS_WITH operator encountered malformed AttributeValue: {}", v2));
|
||||
}
|
||||
auto it2 = v2.MemberBegin();
|
||||
if (it2->name != "S" && it2->name != "B") {
|
||||
throw api_error::validation(format("BEGINS_WITH operator requires String or Binary type in AttributeValue, got {}", it2->name));
|
||||
throw api_error("ValidationException", format("BEGINS_WITH operator requires String or Binary in AttributeValue, got {}", it2->name));
|
||||
}
|
||||
|
||||
|
||||
@@ -180,25 +213,40 @@ static bool check_BEGINS_WITH(const rjson::value* v1, const rjson::value& v2) {
|
||||
return false;
|
||||
}
|
||||
if (it2->name == "S") {
|
||||
return rjson::to_string_view(it1->value).starts_with(rjson::to_string_view(it2->value));
|
||||
std::string_view val1(it1->value.GetString(), it1->value.GetStringLength());
|
||||
std::string_view val2(it2->value.GetString(), it2->value.GetStringLength());
|
||||
return val1.substr(0, val2.size()) == val2;
|
||||
} else /* it2->name == "B" */ {
|
||||
return base64_begins_with(rjson::to_string_view(it1->value), rjson::to_string_view(it2->value));
|
||||
// TODO (optimization): Check the begins_with condition directly on
|
||||
// the base64-encoded string, without making a decoded copy.
|
||||
bytes val1 = base64_decode(it1->value);
|
||||
bytes val2 = base64_decode(it2->value);
|
||||
return val1.substr(0, val2.size()) == val2;
|
||||
}
|
||||
}
|
||||
|
||||
static std::string_view to_string_view(const rjson::value& v) {
|
||||
return std::string_view(v.GetString(), v.GetStringLength());
|
||||
}
|
||||
|
||||
static bool is_set_of(const rjson::value& type1, const rjson::value& type2) {
|
||||
return (type2 == "S" && type1 == "SS") || (type2 == "N" && type1 == "NS") || (type2 == "B" && type1 == "BS");
|
||||
}
|
||||
|
||||
// Check if two JSON-encoded values match with the CONTAINS relation
|
||||
bool check_CONTAINS(const rjson::value* v1, const rjson::value& v2) {
|
||||
static bool check_CONTAINS(const rjson::value* v1, const rjson::value& v2) {
|
||||
if (!v1) {
|
||||
return false;
|
||||
}
|
||||
const auto& kv1 = *v1->MemberBegin();
|
||||
const auto& kv2 = *v2.MemberBegin();
|
||||
if (kv2.name != "S" && kv2.name != "N" && kv2.name != "B") {
|
||||
throw api_error("ValidationException",
|
||||
format("CONTAINS operator requires a single AttributeValue of type String, Number, or Binary, "
|
||||
"got {} instead", kv2.name));
|
||||
}
|
||||
if (kv1.name == "S" && kv2.name == "S") {
|
||||
return rjson::to_string_view(kv1.value).find(rjson::to_string_view(kv2.value)) != std::string_view::npos;
|
||||
return to_string_view(kv1.value).find(to_string_view(kv2.value)) != std::string_view::npos;
|
||||
} else if (kv1.name == "B" && kv2.name == "B") {
|
||||
return base64_decode(kv1.value).find(base64_decode(kv2.value)) != bytes::npos;
|
||||
} else if (is_set_of(kv1.name, kv2.name)) {
|
||||
@@ -233,12 +281,12 @@ static bool check_NOT_CONTAINS(const rjson::value* v1, const rjson::value& v2) {
|
||||
// Check if a JSON-encoded value equals any element of an array, which must have at least one element.
|
||||
static bool check_IN(const rjson::value* val, const rjson::value& array) {
|
||||
if (!array[0].IsObject() || array[0].MemberCount() != 1) {
|
||||
throw api_error::validation(
|
||||
throw api_error("ValidationException",
|
||||
format("IN operator encountered malformed AttributeValue: {}", array[0]));
|
||||
}
|
||||
const auto& type = array[0].MemberBegin()->name;
|
||||
if (type != "S" && type != "N" && type != "B") {
|
||||
throw api_error::validation(
|
||||
throw api_error("ValidationException",
|
||||
"IN operator requires AttributeValueList elements to be of type String, Number, or Binary ");
|
||||
}
|
||||
if (!val) {
|
||||
@@ -247,7 +295,7 @@ static bool check_IN(const rjson::value* val, const rjson::value& array) {
|
||||
bool have_match = false;
|
||||
for (const auto& elem : array.GetArray()) {
|
||||
if (!elem.IsObject() || elem.MemberCount() != 1 || elem.MemberBegin()->name != type) {
|
||||
throw api_error::validation(
|
||||
throw api_error("ValidationException",
|
||||
"IN operator requires all AttributeValueList elements to have the same type ");
|
||||
}
|
||||
if (!have_match && *val == elem) {
|
||||
@@ -258,19 +306,6 @@ static bool check_IN(const rjson::value* val, const rjson::value& array) {
|
||||
return have_match;
|
||||
}
|
||||
|
||||
// Another variant of check_IN, this one for ConditionExpression. It needs to
|
||||
// check whether the first element in the given vector is equal to any of the
|
||||
// others.
|
||||
static bool check_IN(const std::vector<rjson::value>& array) {
|
||||
const rjson::value* first = &array[0];
|
||||
for (unsigned i = 1; i < array.size(); i++) {
|
||||
if (check_EQ(first, array[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool check_NULL(const rjson::value* val) {
|
||||
return val == nullptr;
|
||||
}
|
||||
@@ -283,13 +318,13 @@ static bool check_NOT_NULL(const rjson::value* val) {
|
||||
template <typename Comparator>
|
||||
bool check_compare(const rjson::value* v1, const rjson::value& v2, const Comparator& cmp) {
|
||||
if (!v2.IsObject() || v2.MemberCount() != 1) {
|
||||
throw api_error::validation(
|
||||
throw api_error("ValidationException",
|
||||
format("{} requires a single AttributeValue of type String, Number, or Binary",
|
||||
cmp.diagnostic));
|
||||
}
|
||||
const auto& kv2 = *v2.MemberBegin();
|
||||
if (kv2.name != "S" && kv2.name != "N" && kv2.name != "B") {
|
||||
throw api_error::validation(
|
||||
throw api_error("ValidationException",
|
||||
format("{} requires a single AttributeValue of type String, Number, or Binary",
|
||||
cmp.diagnostic));
|
||||
}
|
||||
@@ -316,36 +351,32 @@ bool check_compare(const rjson::value* v1, const rjson::value& v2, const Compara
|
||||
|
||||
struct cmp_lt {
|
||||
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return lhs < rhs; }
|
||||
// We cannot use the normal comparison operators like "<" on the bytes
|
||||
// type, because they treat individual bytes as signed but we need to
|
||||
// compare them as *unsigned*. So we need a specialization for bytes.
|
||||
bool operator()(const bytes& lhs, const bytes& rhs) const { return compare_unsigned(lhs, rhs) < 0; }
|
||||
static constexpr const char* diagnostic = "LT operator";
|
||||
};
|
||||
|
||||
struct cmp_le {
|
||||
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return lhs <= rhs; }
|
||||
bool operator()(const bytes& lhs, const bytes& rhs) const { return compare_unsigned(lhs, rhs) <= 0; }
|
||||
// bytes only has <, so we cannot use <=.
|
||||
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return lhs < rhs || lhs == rhs; }
|
||||
static constexpr const char* diagnostic = "LE operator";
|
||||
};
|
||||
|
||||
struct cmp_ge {
|
||||
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return lhs >= rhs; }
|
||||
bool operator()(const bytes& lhs, const bytes& rhs) const { return compare_unsigned(lhs, rhs) >= 0; }
|
||||
// bytes only has <, so we cannot use >=.
|
||||
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return rhs < lhs || lhs == rhs; }
|
||||
static constexpr const char* diagnostic = "GE operator";
|
||||
};
|
||||
|
||||
struct cmp_gt {
|
||||
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return lhs > rhs; }
|
||||
bool operator()(const bytes& lhs, const bytes& rhs) const { return compare_unsigned(lhs, rhs) > 0; }
|
||||
// bytes only has <, so we cannot use >.
|
||||
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return rhs < lhs; }
|
||||
static constexpr const char* diagnostic = "GT operator";
|
||||
};
|
||||
|
||||
// True if v is between lb and ub, inclusive. Throws if lb > ub.
|
||||
template <typename T>
|
||||
static bool check_BETWEEN(const T& v, const T& lb, const T& ub) {
|
||||
if (cmp_lt()(ub, lb)) {
|
||||
throw api_error::validation(
|
||||
bool check_BETWEEN(const T& v, const T& lb, const T& ub) {
|
||||
if (ub < lb) {
|
||||
throw api_error("ValidationException",
|
||||
format("BETWEEN operator requires lower_bound <= upper_bound, but {} > {}", lb, ub));
|
||||
}
|
||||
return cmp_ge()(v, lb) && cmp_le()(v, ub);
|
||||
@@ -356,20 +387,21 @@ static bool check_BETWEEN(const rjson::value* v, const rjson::value& lb, const r
|
||||
return false;
|
||||
}
|
||||
if (!v->IsObject() || v->MemberCount() != 1) {
|
||||
throw api_error::validation(format("BETWEEN operator encountered malformed AttributeValue: {}", *v));
|
||||
throw api_error("ValidationException", format("BETWEEN operator encountered malformed AttributeValue: {}", *v));
|
||||
}
|
||||
if (!lb.IsObject() || lb.MemberCount() != 1) {
|
||||
throw api_error::validation(format("BETWEEN operator encountered malformed AttributeValue: {}", lb));
|
||||
throw api_error("ValidationException", format("BETWEEN operator encountered malformed AttributeValue: {}", lb));
|
||||
}
|
||||
if (!ub.IsObject() || ub.MemberCount() != 1) {
|
||||
throw api_error::validation(format("BETWEEN operator encountered malformed AttributeValue: {}", ub));
|
||||
throw api_error("ValidationException", format("BETWEEN operator encountered malformed AttributeValue: {}", ub));
|
||||
}
|
||||
|
||||
const auto& kv_v = *v->MemberBegin();
|
||||
const auto& kv_lb = *lb.MemberBegin();
|
||||
const auto& kv_ub = *ub.MemberBegin();
|
||||
if (kv_lb.name != kv_ub.name) {
|
||||
throw api_error::validation(
|
||||
throw api_error(
|
||||
"ValidationException",
|
||||
format("BETWEEN operator requires the same type for lower and upper bound; instead got {} and {}",
|
||||
kv_lb.name, kv_ub.name));
|
||||
}
|
||||
@@ -388,7 +420,7 @@ static bool check_BETWEEN(const rjson::value* v, const rjson::value& lb, const r
|
||||
if (kv_v.name == "B") {
|
||||
return check_BETWEEN(base64_decode(kv_v.value), base64_decode(kv_lb.value), base64_decode(kv_ub.value));
|
||||
}
|
||||
throw api_error::validation(
|
||||
throw api_error("ValidationException",
|
||||
format("BETWEEN operator requires AttributeValueList elements to be of type String, Number, or Binary; instead got {}",
|
||||
kv_lb.name));
|
||||
}
|
||||
@@ -408,24 +440,24 @@ static bool verify_expected_one(const rjson::value& condition, const rjson::valu
|
||||
// and requires a different combinations of parameters in the request
|
||||
if (value) {
|
||||
if (exists && (!exists->IsBool() || exists->GetBool() != true)) {
|
||||
throw api_error::validation("Cannot combine Value with Exists!=true");
|
||||
throw api_error("ValidationException", "Cannot combine Value with Exists!=true");
|
||||
}
|
||||
if (comparison_operator) {
|
||||
throw api_error::validation("Cannot combine Value with ComparisonOperator");
|
||||
throw api_error("ValidationException", "Cannot combine Value with ComparisonOperator");
|
||||
}
|
||||
return check_EQ(got, *value);
|
||||
} else if (exists) {
|
||||
if (comparison_operator) {
|
||||
throw api_error::validation("Cannot combine Exists with ComparisonOperator");
|
||||
throw api_error("ValidationException", "Cannot combine Exists with ComparisonOperator");
|
||||
}
|
||||
if (!exists->IsBool() || exists->GetBool() != false) {
|
||||
throw api_error::validation("Exists!=false requires Value");
|
||||
throw api_error("ValidationException", "Exists!=false requires Value");
|
||||
}
|
||||
// Remember Exists=false, so we're checking that the attribute does *not* exist:
|
||||
return !got;
|
||||
} else {
|
||||
if (!comparison_operator) {
|
||||
throw api_error::validation("Missing ComparisonOperator, Value or Exists");
|
||||
throw api_error("ValidationException", "Missing ComparisonOperator, Value or Exists");
|
||||
}
|
||||
comparison_operator_type op = get_comparison_operator(*comparison_operator);
|
||||
switch (op) {
|
||||
@@ -463,188 +495,70 @@ static bool verify_expected_one(const rjson::value& condition, const rjson::valu
|
||||
verify_operand_count(attribute_value_list, exact_size(2), *comparison_operator);
|
||||
return check_BETWEEN(got, (*attribute_value_list)[0], (*attribute_value_list)[1]);
|
||||
case comparison_operator_type::CONTAINS:
|
||||
{
|
||||
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
||||
// Expected's "CONTAINS" has this artificial limitation.
|
||||
// ConditionExpression's "contains()" does not...
|
||||
const rjson::value& arg = (*attribute_value_list)[0];
|
||||
const auto& argtype = (*arg.MemberBegin()).name;
|
||||
if (argtype != "S" && argtype != "N" && argtype != "B") {
|
||||
throw api_error::validation(
|
||||
format("CONTAINS operator requires a single AttributeValue of type String, Number, or Binary, "
|
||||
"got {} instead", argtype));
|
||||
}
|
||||
return check_CONTAINS(got, arg);
|
||||
}
|
||||
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
||||
return check_CONTAINS(got, (*attribute_value_list)[0]);
|
||||
case comparison_operator_type::NOT_CONTAINS:
|
||||
{
|
||||
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
||||
// Expected's "NOT_CONTAINS" has this artificial limitation.
|
||||
// ConditionExpression's "contains()" does not...
|
||||
const rjson::value& arg = (*attribute_value_list)[0];
|
||||
const auto& argtype = (*arg.MemberBegin()).name;
|
||||
if (argtype != "S" && argtype != "N" && argtype != "B") {
|
||||
throw api_error::validation(
|
||||
format("CONTAINS operator requires a single AttributeValue of type String, Number, or Binary, "
|
||||
"got {} instead", argtype));
|
||||
}
|
||||
return check_NOT_CONTAINS(got, arg);
|
||||
}
|
||||
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
||||
return check_NOT_CONTAINS(got, (*attribute_value_list)[0]);
|
||||
}
|
||||
throw std::logic_error(format("Internal error: corrupted operator enum: {}", int(op)));
|
||||
}
|
||||
}
|
||||
|
||||
conditional_operator_type get_conditional_operator(const rjson::value& req) {
|
||||
const rjson::value* conditional_operator = rjson::find(req, "ConditionalOperator");
|
||||
if (!conditional_operator) {
|
||||
return conditional_operator_type::MISSING;
|
||||
}
|
||||
if (!conditional_operator->IsString()) {
|
||||
throw api_error::validation("'ConditionalOperator' parameter, if given, must be a string");
|
||||
}
|
||||
auto s = rjson::to_string_view(*conditional_operator);
|
||||
if (s == "AND") {
|
||||
return conditional_operator_type::AND;
|
||||
} else if (s == "OR") {
|
||||
return conditional_operator_type::OR;
|
||||
} else {
|
||||
throw api_error::validation(
|
||||
format("'ConditionalOperator' parameter must be AND, OR or missing. Found {}.", s));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the existing values of the item (previous_item) match the
|
||||
// Verify that the existing values of the item (previous_item) match the
|
||||
// conditions given by the Expected and ConditionalOperator parameters
|
||||
// (if they exist) in the request (an UpdateItem, PutItem or DeleteItem).
|
||||
// This function can throw an ValidationException API error if there
|
||||
// This function will throw a ConditionalCheckFailedException API error
|
||||
// if the values do not match the condition, or ValidationException if there
|
||||
// are errors in the format of the condition itself.
|
||||
bool verify_expected(const rjson::value& req, const rjson::value* previous_item) {
|
||||
void verify_expected(const rjson::value& req, const std::unique_ptr<rjson::value>& previous_item) {
|
||||
const rjson::value* expected = rjson::find(req, "Expected");
|
||||
auto conditional_operator = get_conditional_operator(req);
|
||||
if (conditional_operator != conditional_operator_type::MISSING &&
|
||||
(!expected || (expected->IsObject() && expected->GetObject().ObjectEmpty()))) {
|
||||
throw api_error::validation("'ConditionalOperator' parameter cannot be specified for missing or empty Expression");
|
||||
}
|
||||
if (!expected) {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
if (!expected->IsObject()) {
|
||||
throw api_error::validation("'Expected' parameter, if given, must be an object");
|
||||
throw api_error("ValidationException", "'Expected' parameter, if given, must be an object");
|
||||
}
|
||||
// ConditionalOperator can be "AND" for requiring all conditions, or
|
||||
// "OR" for requiring one condition, and defaults to "AND" if missing.
|
||||
const rjson::value* conditional_operator = rjson::find(req, "ConditionalOperator");
|
||||
bool require_all = true;
|
||||
if (conditional_operator) {
|
||||
if (!conditional_operator->IsString()) {
|
||||
throw api_error("ValidationException", "'ConditionalOperator' parameter, if given, must be a string");
|
||||
}
|
||||
std::string_view s(conditional_operator->GetString(), conditional_operator->GetStringLength());
|
||||
if (s == "AND") {
|
||||
// require_all is already true
|
||||
} else if (s == "OR") {
|
||||
require_all = false;
|
||||
} else {
|
||||
throw api_error("ValidationException", "'ConditionalOperator' parameter must be AND, OR or missing");
|
||||
}
|
||||
if (expected->GetObject().ObjectEmpty()) {
|
||||
throw api_error("ValidationException", "'ConditionalOperator' parameter cannot be specified for empty Expression");
|
||||
}
|
||||
}
|
||||
bool require_all = conditional_operator != conditional_operator_type::OR;
|
||||
return verify_condition(*expected, require_all, previous_item);
|
||||
}
|
||||
|
||||
bool verify_condition(const rjson::value& condition, bool require_all, const rjson::value* previous_item) {
|
||||
for (auto it = condition.MemberBegin(); it != condition.MemberEnd(); ++it) {
|
||||
for (auto it = expected->MemberBegin(); it != expected->MemberEnd(); ++it) {
|
||||
const rjson::value* got = nullptr;
|
||||
if (previous_item) {
|
||||
got = rjson::find(*previous_item, rjson::to_string_view(it->name));
|
||||
if (previous_item && previous_item->IsObject() && previous_item->HasMember("Item")) {
|
||||
got = rjson::find((*previous_item)["Item"], rjson::string_ref_type(it->name.GetString()));
|
||||
}
|
||||
bool success = verify_expected_one(it->value, got);
|
||||
if (success && !require_all) {
|
||||
// When !require_all, one success is enough!
|
||||
return true;
|
||||
return;
|
||||
} else if (!success && require_all) {
|
||||
// When require_all, one failure is enough!
|
||||
return false;
|
||||
throw api_error("ConditionalCheckFailedException", "Failed condition.");
|
||||
}
|
||||
}
|
||||
// If we got here and require_all, none of the checks failed, so succeed.
|
||||
// If we got here and !require_all, all of the checks failed, so fail.
|
||||
return require_all;
|
||||
}
|
||||
|
||||
static bool calculate_primitive_condition(const parsed::primitive_condition& cond,
|
||||
const rjson::value* previous_item) {
|
||||
std::vector<rjson::value> calculated_values;
|
||||
calculated_values.reserve(cond._values.size());
|
||||
for (const parsed::value& v : cond._values) {
|
||||
calculated_values.push_back(calculate_value(v,
|
||||
cond._op == parsed::primitive_condition::type::VALUE ?
|
||||
calculate_value_caller::ConditionExpressionAlone :
|
||||
calculate_value_caller::ConditionExpression,
|
||||
previous_item));
|
||||
}
|
||||
switch (cond._op) {
|
||||
case parsed::primitive_condition::type::BETWEEN:
|
||||
if (calculated_values.size() != 3) {
|
||||
// Shouldn't happen unless we have a bug in the parser
|
||||
throw std::logic_error(format("Wrong number of values {} in BETWEEN primitive_condition", cond._values.size()));
|
||||
}
|
||||
return check_BETWEEN(&calculated_values[0], calculated_values[1], calculated_values[2]);
|
||||
case parsed::primitive_condition::type::IN:
|
||||
return check_IN(calculated_values);
|
||||
case parsed::primitive_condition::type::VALUE:
|
||||
if (calculated_values.size() != 1) {
|
||||
// Shouldn't happen unless we have a bug in the parser
|
||||
throw std::logic_error(format("Unexpected values in primitive_condition", cond._values.size()));
|
||||
}
|
||||
// Unwrap the boolean wrapped as the value (if it is a boolean)
|
||||
if (calculated_values[0].IsObject() && calculated_values[0].MemberCount() == 1) {
|
||||
auto it = calculated_values[0].MemberBegin();
|
||||
if (it->name == "BOOL" && it->value.IsBool()) {
|
||||
return it->value.GetBool();
|
||||
}
|
||||
}
|
||||
throw api_error::validation(
|
||||
format("ConditionExpression: condition results in a non-boolean value: {}",
|
||||
calculated_values[0]));
|
||||
default:
|
||||
// All the rest of the operators have exactly two parameters (and unless
|
||||
// we have a bug in the parser, that's what we have in the parsed object:
|
||||
if (calculated_values.size() != 2) {
|
||||
throw std::logic_error(format("Wrong number of values {} in primitive_condition object", cond._values.size()));
|
||||
}
|
||||
}
|
||||
switch (cond._op) {
|
||||
case parsed::primitive_condition::type::EQ:
|
||||
return check_EQ(&calculated_values[0], calculated_values[1]);
|
||||
case parsed::primitive_condition::type::NE:
|
||||
return check_NE(&calculated_values[0], calculated_values[1]);
|
||||
case parsed::primitive_condition::type::GT:
|
||||
return check_compare(&calculated_values[0], calculated_values[1], cmp_gt{});
|
||||
case parsed::primitive_condition::type::GE:
|
||||
return check_compare(&calculated_values[0], calculated_values[1], cmp_ge{});
|
||||
case parsed::primitive_condition::type::LT:
|
||||
return check_compare(&calculated_values[0], calculated_values[1], cmp_lt{});
|
||||
case parsed::primitive_condition::type::LE:
|
||||
return check_compare(&calculated_values[0], calculated_values[1], cmp_le{});
|
||||
default:
|
||||
// Shouldn't happen unless we have a bug in the parser
|
||||
throw std::logic_error(format("Unknown type {} in primitive_condition object", (int)(cond._op)));
|
||||
if (!require_all) {
|
||||
throw api_error("ConditionalCheckFailedException", "None of ORed Expect conditions were successful.");
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the existing values of the item (previous_item) match the
|
||||
// conditions given by the given parsed ConditionExpression.
|
||||
bool verify_condition_expression(
|
||||
const parsed::condition_expression& condition_expression,
|
||||
const rjson::value* previous_item) {
|
||||
if (condition_expression.empty()) {
|
||||
return true;
|
||||
}
|
||||
bool ret = std::visit(overloaded_functor {
|
||||
[&] (const parsed::primitive_condition& cond) -> bool {
|
||||
return calculate_primitive_condition(cond, previous_item);
|
||||
},
|
||||
[&] (const parsed::condition_expression::condition_list& list) -> bool {
|
||||
auto verify_condition = [&] (const parsed::condition_expression& e) {
|
||||
return verify_condition_expression(e, previous_item);
|
||||
};
|
||||
switch (list.op) {
|
||||
case '&':
|
||||
return boost::algorithm::all_of(list.conditions, verify_condition);
|
||||
case '|':
|
||||
return boost::algorithm::any_of(list.conditions, verify_condition);
|
||||
default:
|
||||
// Shouldn't happen unless we have a bug in the parser
|
||||
throw std::logic_error("bad operator in condition_list");
|
||||
}
|
||||
}
|
||||
}, condition_expression._expression);
|
||||
return condition_expression._negated ? !ret : ret;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
|
||||
#include "cql3/restrictions/statement_restrictions.hh"
|
||||
#include "serialization.hh"
|
||||
#include "expressions_types.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
@@ -43,18 +42,8 @@ enum class comparison_operator_type {
|
||||
|
||||
comparison_operator_type get_comparison_operator(const rjson::value& comparison_operator);
|
||||
|
||||
enum class conditional_operator_type {
|
||||
AND, OR, MISSING
|
||||
};
|
||||
conditional_operator_type get_conditional_operator(const rjson::value& req);
|
||||
::shared_ptr<cql3::restrictions::statement_restrictions> get_filtering_restrictions(schema_ptr schema, const column_definition& attrs_col, const rjson::value& query_filter);
|
||||
|
||||
bool verify_expected(const rjson::value& req, const rjson::value* previous_item);
|
||||
bool verify_condition(const rjson::value& condition, bool require_all, const rjson::value* previous_item);
|
||||
|
||||
bool check_CONTAINS(const rjson::value* v1, const rjson::value& v2);
|
||||
|
||||
bool verify_condition_expression(
|
||||
const parsed::condition_expression& condition_expression,
|
||||
const rjson::value* previous_item);
|
||||
void verify_expected(const rjson::value& req, const std::unique_ptr<rjson::value>& previous_item);
|
||||
|
||||
}
|
||||
|
||||
@@ -26,15 +26,12 @@
|
||||
|
||||
namespace alternator {
|
||||
|
||||
// api_error contains a DynamoDB error message to be returned to the user.
|
||||
// It can be returned by value (see executor::request_return_type) or thrown.
|
||||
// The DynamoDB's error messages are described in detail in
|
||||
// DynamoDB's error messages are described in detail in
|
||||
// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.Errors.html
|
||||
// An error message has an HTTP code (almost always 400), a type, e.g.,
|
||||
// "ResourceNotFoundException", and a human readable message.
|
||||
// Eventually alternator::api_handler will convert a returned or thrown
|
||||
// api_error into a JSON object, and that is returned to the user.
|
||||
class api_error final {
|
||||
// Ah An error message has a "type", e.g., "ResourceNotFoundException", a coarser
|
||||
// HTTP code (almost always, 400), and a human readable message. Eventually these
|
||||
// will be wrapped into a JSON object returned to the client.
|
||||
class api_error : public std::exception {
|
||||
public:
|
||||
using status_type = httpd::reply::status_type;
|
||||
status_type _http_code;
|
||||
@@ -45,41 +42,8 @@ public:
|
||||
, _type(std::move(type))
|
||||
, _msg(std::move(msg))
|
||||
{ }
|
||||
|
||||
// Factory functions for some common types of DynamoDB API errors
|
||||
static api_error validation(std::string msg) {
|
||||
return api_error("ValidationException", std::move(msg));
|
||||
}
|
||||
static api_error resource_not_found(std::string msg) {
|
||||
return api_error("ResourceNotFoundException", std::move(msg));
|
||||
}
|
||||
static api_error resource_in_use(std::string msg) {
|
||||
return api_error("ResourceInUseException", std::move(msg));
|
||||
}
|
||||
static api_error invalid_signature(std::string msg) {
|
||||
return api_error("InvalidSignatureException", std::move(msg));
|
||||
}
|
||||
static api_error unrecognized_client(std::string msg) {
|
||||
return api_error("UnrecognizedClientException", std::move(msg));
|
||||
}
|
||||
static api_error unknown_operation(std::string msg) {
|
||||
return api_error("UnknownOperationException", std::move(msg));
|
||||
}
|
||||
static api_error access_denied(std::string msg) {
|
||||
return api_error("AccessDeniedException", std::move(msg));
|
||||
}
|
||||
static api_error conditional_check_failed(std::string msg) {
|
||||
return api_error("ConditionalCheckFailedException", std::move(msg));
|
||||
}
|
||||
static api_error expired_iterator(std::string msg) {
|
||||
return api_error("ExpiredIteratorException", std::move(msg));
|
||||
}
|
||||
static api_error trimmed_data_access_exception(std::string msg) {
|
||||
return api_error("TrimmedDataAccessException", std::move(msg));
|
||||
}
|
||||
static api_error internal(std::string msg) {
|
||||
return api_error("InternalServerError", std::move(msg), reply::status_type::internal_server_error);
|
||||
}
|
||||
api_error() = default;
|
||||
virtual const char* what() const noexcept override { return _msg.c_str(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,130 +25,47 @@
|
||||
#include <seastar/http/httpd.hh>
|
||||
#include "seastarx.hh"
|
||||
#include <seastar/json/json_elements.hh>
|
||||
#include <seastar/core/sharded.hh>
|
||||
|
||||
#include "service/storage_proxy.hh"
|
||||
#include "service/migration_manager.hh"
|
||||
#include "service/client_state.hh"
|
||||
#include "db/timeout_clock.hh"
|
||||
|
||||
#include "alternator/error.hh"
|
||||
#include "stats.hh"
|
||||
#include "utils/rjson.hh"
|
||||
|
||||
namespace db {
|
||||
class system_distributed_keyspace;
|
||||
}
|
||||
|
||||
namespace query {
|
||||
class partition_slice;
|
||||
class result;
|
||||
}
|
||||
|
||||
namespace cql3::selection {
|
||||
class selection;
|
||||
}
|
||||
|
||||
namespace service {
|
||||
class storage_service;
|
||||
}
|
||||
|
||||
namespace alternator {
|
||||
|
||||
class rmw_operation;
|
||||
|
||||
struct make_jsonable : public json::jsonable {
|
||||
rjson::value _value;
|
||||
public:
|
||||
explicit make_jsonable(rjson::value&& value);
|
||||
std::string to_json() const override;
|
||||
};
|
||||
struct json_string : public json::jsonable {
|
||||
std::string _value;
|
||||
public:
|
||||
explicit json_string(std::string&& value);
|
||||
std::string to_json() const override;
|
||||
};
|
||||
|
||||
class executor : public peering_sharded_service<executor> {
|
||||
class executor {
|
||||
service::storage_proxy& _proxy;
|
||||
service::migration_manager& _mm;
|
||||
db::system_distributed_keyspace& _sdks;
|
||||
service::storage_service& _ss;
|
||||
// An smp_service_group to be used for limiting the concurrency when
|
||||
// forwarding Alternator request between shards - if necessary for LWT.
|
||||
smp_service_group _ssg;
|
||||
|
||||
public:
|
||||
using client_state = service::client_state;
|
||||
using request_return_type = std::variant<json::json_return_type, api_error>;
|
||||
stats _stats;
|
||||
static constexpr auto ATTRS_COLUMN_NAME = ":attrs";
|
||||
static constexpr auto KEYSPACE_NAME_PREFIX = "alternator_";
|
||||
static constexpr std::string_view INTERNAL_TABLE_PREFIX = ".scylla.alternator.";
|
||||
static constexpr auto KEYSPACE_NAME = "alternator";
|
||||
|
||||
executor(service::storage_proxy& proxy, service::migration_manager& mm, db::system_distributed_keyspace& sdks, service::storage_service& ss, smp_service_group ssg)
|
||||
: _proxy(proxy), _mm(mm), _sdks(sdks), _ss(ss), _ssg(ssg) {}
|
||||
executor(service::storage_proxy& proxy, service::migration_manager& mm) : _proxy(proxy), _mm(mm) {}
|
||||
|
||||
future<request_return_type> create_table(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> describe_table(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> delete_table(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> update_table(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> put_item(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> get_item(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> delete_item(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> update_item(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> list_tables(client_state& client_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> scan(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> describe_endpoints(client_state& client_state, service_permit permit, rjson::value request, std::string host_header);
|
||||
future<request_return_type> batch_write_item(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> batch_get_item(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> query(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> tag_resource(client_state& client_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> untag_resource(client_state& client_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> list_tags_of_resource(client_state& client_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> list_streams(client_state& client_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> describe_stream(client_state& client_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> get_shard_iterator(client_state& client_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> get_records(client_state& client_state, tracing::trace_state_ptr, service_permit permit, rjson::value request);
|
||||
future<json::json_return_type> create_table(client_state& client_state, tracing::trace_state_ptr trace_state, std::string content);
|
||||
future<json::json_return_type> describe_table(client_state& client_state, tracing::trace_state_ptr trace_state, std::string content);
|
||||
future<json::json_return_type> delete_table(client_state& client_state, tracing::trace_state_ptr trace_state, std::string content);
|
||||
future<json::json_return_type> put_item(client_state& client_state, tracing::trace_state_ptr trace_state, std::string content);
|
||||
future<json::json_return_type> get_item(client_state& client_state, tracing::trace_state_ptr trace_state, std::string content);
|
||||
future<json::json_return_type> delete_item(client_state& client_state, tracing::trace_state_ptr trace_state, std::string content);
|
||||
future<json::json_return_type> update_item(client_state& client_state, tracing::trace_state_ptr trace_state, std::string content);
|
||||
future<json::json_return_type> list_tables(client_state& client_state, std::string content);
|
||||
future<json::json_return_type> scan(client_state& client_state, tracing::trace_state_ptr trace_state, std::string content);
|
||||
future<json::json_return_type> describe_endpoints(client_state& client_state, std::string content, std::string host_header);
|
||||
future<json::json_return_type> batch_write_item(client_state& client_state, tracing::trace_state_ptr trace_state, std::string content);
|
||||
future<json::json_return_type> batch_get_item(client_state& client_state, tracing::trace_state_ptr trace_state, std::string content);
|
||||
future<json::json_return_type> query(client_state& client_state, tracing::trace_state_ptr trace_state, std::string content);
|
||||
|
||||
future<> start();
|
||||
future<> stop() { return make_ready_future<>(); }
|
||||
|
||||
future<> create_keyspace(std::string_view keyspace_name);
|
||||
future<> maybe_create_keyspace();
|
||||
|
||||
static tracing::trace_state_ptr maybe_trace_query(client_state& client_state, sstring_view op, sstring_view query);
|
||||
|
||||
static sstring table_name(const schema&);
|
||||
static db::timeout_clock::time_point default_timeout();
|
||||
static schema_ptr find_table(service::storage_proxy&, const rjson::value& request);
|
||||
|
||||
private:
|
||||
friend class rmw_operation;
|
||||
|
||||
static bool is_alternator_keyspace(const sstring& ks_name);
|
||||
static sstring make_keyspace_name(const sstring& table_name);
|
||||
static void describe_key_schema(rjson::value& parent, const schema&, std::unordered_map<std::string,std::string> * = nullptr);
|
||||
static void describe_key_schema(rjson::value& parent, const schema& schema, std::unordered_map<std::string,std::string>&);
|
||||
|
||||
public:
|
||||
static std::optional<rjson::value> describe_single_item(schema_ptr,
|
||||
const query::partition_slice&,
|
||||
const cql3::selection::selection&,
|
||||
const query::result&,
|
||||
const std::unordered_set<std::string>&);
|
||||
|
||||
static void describe_single_item(const cql3::selection::selection&,
|
||||
const std::vector<bytes_opt>&,
|
||||
const std::unordered_set<std::string>&,
|
||||
rjson::value&,
|
||||
bool = false);
|
||||
|
||||
|
||||
|
||||
void add_stream_options(const rjson::value& stream_spec, schema_builder&) const;
|
||||
void supplement_table_info(rjson::value& descr, const schema& schema) const;
|
||||
void supplement_table_stream_info(rjson::value& descr, const schema& schema) const;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -20,24 +20,15 @@
|
||||
*/
|
||||
|
||||
#include "expressions.hh"
|
||||
#include "serialization.hh"
|
||||
#include "base64.hh"
|
||||
#include "conditions.hh"
|
||||
#include "alternator/expressionsLexer.hpp"
|
||||
#include "alternator/expressionsParser.hpp"
|
||||
#include "utils/overloaded_functor.hh"
|
||||
#include "error.hh"
|
||||
|
||||
#include "seastarx.hh"
|
||||
#include <seastarx.hh>
|
||||
|
||||
#include <seastar/core/print.hh>
|
||||
#include <seastar/util/log.hh>
|
||||
|
||||
#include <boost/algorithm/cxx11/any_of.hpp>
|
||||
#include <boost/algorithm/cxx11/all_of.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace alternator {
|
||||
|
||||
@@ -74,19 +65,13 @@ parse_projection_expression(std::string query) {
|
||||
}
|
||||
}
|
||||
|
||||
parsed::condition_expression
|
||||
parse_condition_expression(std::string query) {
|
||||
try {
|
||||
return do_with_parser(query, std::mem_fn(&expressionsParser::condition_expression));
|
||||
} catch (...) {
|
||||
throw expressions_syntax_error(format("Failed parsing ConditionExpression '{}': {}", query, std::current_exception()));
|
||||
}
|
||||
}
|
||||
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
|
||||
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
|
||||
|
||||
namespace parsed {
|
||||
|
||||
void update_expression::add(update_expression::action a) {
|
||||
std::visit(overloaded_functor {
|
||||
std::visit(overloaded {
|
||||
[&] (action::set&) { seen_set = true; },
|
||||
[&] (action::remove&) { seen_remove = true; },
|
||||
[&] (action::add&) { seen_add = true; },
|
||||
@@ -109,620 +94,5 @@ void update_expression::append(update_expression other) {
|
||||
seen_del |= other.seen_del;
|
||||
}
|
||||
|
||||
void condition_expression::append(condition_expression&& a, char op) {
|
||||
std::visit(overloaded_functor {
|
||||
[&] (condition_list& x) {
|
||||
// If 'a' has a single condition, we could, instead of inserting
|
||||
// it insert its single condition (possibly negated if a._negated)
|
||||
// But considering it we don't evaluate these expressions many
|
||||
// times, this optimization is not worth extra code complexity.
|
||||
if (!x.conditions.empty() && x.op != op) {
|
||||
// Shouldn't happen unless we have a bug in the parser
|
||||
throw std::logic_error("condition_expression::append called with mixed operators");
|
||||
}
|
||||
x.conditions.push_back(std::move(a));
|
||||
x.op = op;
|
||||
},
|
||||
[&] (primitive_condition& x) {
|
||||
// Shouldn't happen unless we have a bug in the parser
|
||||
throw std::logic_error("condition_expression::append called on primitive_condition");
|
||||
}
|
||||
}, _expression);
|
||||
}
|
||||
|
||||
} // namespace parsed
|
||||
|
||||
// The following resolve_*() functions resolve references in parsed
|
||||
// expressions of different types. Resolving a parsed expression means
|
||||
// replacing:
|
||||
// 1. In parsed::path objects, replace references like "#name" with the
|
||||
// attribute name from ExpressionAttributeNames,
|
||||
// 2. In parsed::constant objects, replace references like ":value" with
|
||||
// the value from ExpressionAttributeValues.
|
||||
// These function also track which name and value references were used, to
|
||||
// allow complaining if some remain unused.
|
||||
// Note that the resolve_*() functions modify the expressions in-place,
|
||||
// so if we ever intend to cache parsed expression, we need to pass a copy
|
||||
// into this function.
|
||||
//
|
||||
// Doing the "resolving" stage before the evaluation stage has two benefits.
|
||||
// First, it allows us to be compatible with DynamoDB in catching unused
|
||||
// names and values (see issue #6572). Second, in the FilterExpression case,
|
||||
// we need to resolve the expression just once but then use it many times
|
||||
// (once for each item to be filtered).
|
||||
|
||||
static void resolve_path(parsed::path& p,
|
||||
const rjson::value* expression_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_names) {
|
||||
const std::string& column_name = p.root();
|
||||
if (column_name.size() > 0 && column_name.front() == '#') {
|
||||
if (!expression_attribute_names) {
|
||||
throw api_error::validation(
|
||||
format("ExpressionAttributeNames missing, entry '{}' required by expression", column_name));
|
||||
}
|
||||
const rjson::value* value = rjson::find(*expression_attribute_names, column_name);
|
||||
if (!value || !value->IsString()) {
|
||||
throw api_error::validation(
|
||||
format("ExpressionAttributeNames missing entry '{}' required by expression", column_name));
|
||||
}
|
||||
used_attribute_names.emplace(column_name);
|
||||
p.set_root(std::string(rjson::to_string_view(*value)));
|
||||
}
|
||||
}
|
||||
|
||||
static void resolve_constant(parsed::constant& c,
|
||||
const rjson::value* expression_attribute_values,
|
||||
std::unordered_set<std::string>& used_attribute_values) {
|
||||
std::visit(overloaded_functor {
|
||||
[&] (const std::string& valref) {
|
||||
if (!expression_attribute_values) {
|
||||
throw api_error::validation(
|
||||
format("ExpressionAttributeValues missing, entry '{}' required by expression", valref));
|
||||
}
|
||||
const rjson::value* value = rjson::find(*expression_attribute_values, valref);
|
||||
if (!value) {
|
||||
throw api_error::validation(
|
||||
format("ExpressionAttributeValues missing entry '{}' required by expression", valref));
|
||||
}
|
||||
if (value->IsNull()) {
|
||||
throw api_error::validation(
|
||||
format("ExpressionAttributeValues null value for entry '{}' required by expression", valref));
|
||||
}
|
||||
validate_value(*value, "ExpressionAttributeValues");
|
||||
used_attribute_values.emplace(valref);
|
||||
c.set(*value);
|
||||
},
|
||||
[&] (const parsed::constant::literal& lit) {
|
||||
// Nothing to do, already resolved
|
||||
}
|
||||
}, c._value);
|
||||
|
||||
}
|
||||
|
||||
void resolve_value(parsed::value& rhs,
|
||||
const rjson::value* expression_attribute_names,
|
||||
const rjson::value* expression_attribute_values,
|
||||
std::unordered_set<std::string>& used_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_values) {
|
||||
std::visit(overloaded_functor {
|
||||
[&] (parsed::constant& c) {
|
||||
resolve_constant(c, expression_attribute_values, used_attribute_values);
|
||||
},
|
||||
[&] (parsed::value::function_call& f) {
|
||||
for (parsed::value& value : f._parameters) {
|
||||
resolve_value(value, expression_attribute_names, expression_attribute_values,
|
||||
used_attribute_names, used_attribute_values);
|
||||
}
|
||||
},
|
||||
[&] (parsed::path& p) {
|
||||
resolve_path(p, expression_attribute_names, used_attribute_names);
|
||||
}
|
||||
}, rhs._value);
|
||||
}
|
||||
|
||||
void resolve_set_rhs(parsed::set_rhs& rhs,
|
||||
const rjson::value* expression_attribute_names,
|
||||
const rjson::value* expression_attribute_values,
|
||||
std::unordered_set<std::string>& used_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_values) {
|
||||
resolve_value(rhs._v1, expression_attribute_names, expression_attribute_values,
|
||||
used_attribute_names, used_attribute_values);
|
||||
if (rhs._op != 'v') {
|
||||
resolve_value(rhs._v2, expression_attribute_names, expression_attribute_values,
|
||||
used_attribute_names, used_attribute_values);
|
||||
}
|
||||
}
|
||||
|
||||
void resolve_update_expression(parsed::update_expression& ue,
|
||||
const rjson::value* expression_attribute_names,
|
||||
const rjson::value* expression_attribute_values,
|
||||
std::unordered_set<std::string>& used_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_values) {
|
||||
for (parsed::update_expression::action& action : ue.actions()) {
|
||||
resolve_path(action._path, expression_attribute_names, used_attribute_names);
|
||||
std::visit(overloaded_functor {
|
||||
[&] (parsed::update_expression::action::set& a) {
|
||||
resolve_set_rhs(a._rhs, expression_attribute_names, expression_attribute_values,
|
||||
used_attribute_names, used_attribute_values);
|
||||
},
|
||||
[&] (parsed::update_expression::action::remove& a) {
|
||||
// nothing to do
|
||||
},
|
||||
[&] (parsed::update_expression::action::add& a) {
|
||||
resolve_constant(a._valref, expression_attribute_values, used_attribute_values);
|
||||
},
|
||||
[&] (parsed::update_expression::action::del& a) {
|
||||
resolve_constant(a._valref, expression_attribute_values, used_attribute_values);
|
||||
}
|
||||
}, action._action);
|
||||
}
|
||||
}
|
||||
|
||||
static void resolve_primitive_condition(parsed::primitive_condition& pc,
|
||||
const rjson::value* expression_attribute_names,
|
||||
const rjson::value* expression_attribute_values,
|
||||
std::unordered_set<std::string>& used_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_values) {
|
||||
for (parsed::value& value : pc._values) {
|
||||
resolve_value(value,
|
||||
expression_attribute_names, expression_attribute_values,
|
||||
used_attribute_names, used_attribute_values);
|
||||
}
|
||||
}
|
||||
|
||||
void resolve_condition_expression(parsed::condition_expression& ce,
|
||||
const rjson::value* expression_attribute_names,
|
||||
const rjson::value* expression_attribute_values,
|
||||
std::unordered_set<std::string>& used_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_values) {
|
||||
std::visit(overloaded_functor {
|
||||
[&] (parsed::primitive_condition& cond) {
|
||||
resolve_primitive_condition(cond,
|
||||
expression_attribute_names, expression_attribute_values,
|
||||
used_attribute_names, used_attribute_values);
|
||||
},
|
||||
[&] (parsed::condition_expression::condition_list& list) {
|
||||
for (parsed::condition_expression& cond : list.conditions) {
|
||||
resolve_condition_expression(cond,
|
||||
expression_attribute_names, expression_attribute_values,
|
||||
used_attribute_names, used_attribute_values);
|
||||
|
||||
}
|
||||
}
|
||||
}, ce._expression);
|
||||
}
|
||||
|
||||
void resolve_projection_expression(std::vector<parsed::path>& pe,
|
||||
const rjson::value* expression_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_names) {
|
||||
for (parsed::path& p : pe) {
|
||||
resolve_path(p, expression_attribute_names, used_attribute_names);
|
||||
}
|
||||
}
|
||||
|
||||
// condition_expression_on() checks whether a condition_expression places any
|
||||
// condition on the given attribute. It can be useful, for example, for
|
||||
// checking whether the condition tries to restrict a key column.
|
||||
|
||||
static bool value_on(const parsed::value& v, std::string_view attribute) {
|
||||
return std::visit(overloaded_functor {
|
||||
[&] (const parsed::constant& c) {
|
||||
return false;
|
||||
},
|
||||
[&] (const parsed::value::function_call& f) {
|
||||
for (const parsed::value& value : f._parameters) {
|
||||
if (value_on(value, attribute)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[&] (const parsed::path& p) {
|
||||
return p.root() == attribute;
|
||||
}
|
||||
}, v._value);
|
||||
}
|
||||
|
||||
static bool primitive_condition_on(const parsed::primitive_condition& pc, std::string_view attribute) {
|
||||
for (const parsed::value& value : pc._values) {
|
||||
if (value_on(value, attribute)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool condition_expression_on(const parsed::condition_expression& ce, std::string_view attribute) {
|
||||
return std::visit(overloaded_functor {
|
||||
[&] (const parsed::primitive_condition& cond) {
|
||||
return primitive_condition_on(cond, attribute);
|
||||
},
|
||||
[&] (const parsed::condition_expression::condition_list& list) {
|
||||
for (const parsed::condition_expression& cond : list.conditions) {
|
||||
if (condition_expression_on(cond, attribute)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}, ce._expression);
|
||||
}
|
||||
|
||||
// for_condition_expression_on() runs a given function over all the attributes
|
||||
// mentioned in the expression. If the same attribute is mentioned more than
|
||||
// once, the function will be called more than once for the same attribute.
|
||||
|
||||
static void for_value_on(const parsed::value& v, const noncopyable_function<void(std::string_view)>& func) {
|
||||
std::visit(overloaded_functor {
|
||||
[&] (const parsed::constant& c) { },
|
||||
[&] (const parsed::value::function_call& f) {
|
||||
for (const parsed::value& value : f._parameters) {
|
||||
for_value_on(value, func);
|
||||
}
|
||||
},
|
||||
[&] (const parsed::path& p) {
|
||||
func(p.root());
|
||||
}
|
||||
}, v._value);
|
||||
}
|
||||
|
||||
void for_condition_expression_on(const parsed::condition_expression& ce, const noncopyable_function<void(std::string_view)>& func) {
|
||||
std::visit(overloaded_functor {
|
||||
[&] (const parsed::primitive_condition& cond) {
|
||||
for (const parsed::value& value : cond._values) {
|
||||
for_value_on(value, func);
|
||||
}
|
||||
},
|
||||
[&] (const parsed::condition_expression::condition_list& list) {
|
||||
for (const parsed::condition_expression& cond : list.conditions) {
|
||||
for_condition_expression_on(cond, func);
|
||||
}
|
||||
}
|
||||
}, ce._expression);
|
||||
}
|
||||
|
||||
// The following calculate_value() functions calculate, or evaluate, a parsed
|
||||
// expression. The parsed expression is assumed to have been "resolved", with
|
||||
// the matching resolve_* function.
|
||||
|
||||
// Take two JSON-encoded list values (remember that a list value is
|
||||
// {"L": [...the actual list]}) and return the concatenation, again as
|
||||
// a list value.
|
||||
static rjson::value list_concatenate(const rjson::value& v1, const rjson::value& v2) {
|
||||
const rjson::value* list1 = unwrap_list(v1);
|
||||
const rjson::value* list2 = unwrap_list(v2);
|
||||
if (!list1 || !list2) {
|
||||
throw api_error::validation("UpdateExpression: list_append() given a non-list");
|
||||
}
|
||||
rjson::value cat = rjson::copy(*list1);
|
||||
for (const auto& a : list2->GetArray()) {
|
||||
rjson::push_back(cat, rjson::copy(a));
|
||||
}
|
||||
rjson::value ret = rjson::empty_object();
|
||||
rjson::set(ret, "L", std::move(cat));
|
||||
return ret;
|
||||
}
|
||||
|
||||
// calculate_size() is ConditionExpression's size() function, i.e., it takes
|
||||
// a JSON-encoded value and returns its "size" as defined differently for the
|
||||
// different types - also as a JSON-encoded number.
|
||||
// It return a JSON-encoded "null" value if this value's type has no size
|
||||
// defined. Comparisons against this non-numeric value will later fail.
|
||||
static rjson::value calculate_size(const rjson::value& v) {
|
||||
// NOTE: If v is improperly formatted for our JSON value encoding, it
|
||||
// must come from the request itself, not from the database, so it makes
|
||||
// sense to throw a ValidationException if we see such a problem.
|
||||
if (!v.IsObject() || v.MemberCount() != 1) {
|
||||
throw api_error::validation(format("invalid object: {}", v));
|
||||
}
|
||||
auto it = v.MemberBegin();
|
||||
int ret;
|
||||
if (it->name == "S") {
|
||||
if (!it->value.IsString()) {
|
||||
throw api_error::validation(format("invalid string: {}", v));
|
||||
}
|
||||
ret = it->value.GetStringLength();
|
||||
} else if (it->name == "NS" || it->name == "SS" || it->name == "BS" || it->name == "L") {
|
||||
if (!it->value.IsArray()) {
|
||||
throw api_error::validation(format("invalid set: {}", v));
|
||||
}
|
||||
ret = it->value.Size();
|
||||
} else if (it->name == "M") {
|
||||
if (!it->value.IsObject()) {
|
||||
throw api_error::validation(format("invalid map: {}", v));
|
||||
}
|
||||
ret = it->value.MemberCount();
|
||||
} else if (it->name == "B") {
|
||||
if (!it->value.IsString()) {
|
||||
throw api_error::validation(format("invalid byte string: {}", v));
|
||||
}
|
||||
ret = base64_decoded_len(rjson::to_string_view(it->value));
|
||||
} else {
|
||||
rjson::value json_ret = rjson::empty_object();
|
||||
rjson::set(json_ret, "null", rjson::value(true));
|
||||
return json_ret;
|
||||
}
|
||||
rjson::value json_ret = rjson::empty_object();
|
||||
rjson::set(json_ret, "N", rjson::from_string(std::to_string(ret)));
|
||||
return json_ret;
|
||||
}
|
||||
|
||||
static const rjson::value& calculate_value(const parsed::constant& c) {
|
||||
return std::visit(overloaded_functor {
|
||||
[&] (const parsed::constant::literal& v) -> const rjson::value& {
|
||||
return *v;
|
||||
},
|
||||
[&] (const std::string& valref) -> const rjson::value& {
|
||||
// Shouldn't happen, we should have called resolve_value() earlier
|
||||
// and replaced the value reference by the literal constant.
|
||||
throw std::logic_error("calculate_value() called before resolve_value()");
|
||||
}
|
||||
}, c._value);
|
||||
}
|
||||
|
||||
static rjson::value to_bool_json(bool b) {
|
||||
rjson::value json_ret = rjson::empty_object();
|
||||
rjson::set(json_ret, "BOOL", rjson::value(b));
|
||||
return json_ret;
|
||||
}
|
||||
|
||||
static bool known_type(std::string_view type) {
|
||||
static thread_local const std::unordered_set<std::string_view> types = {
|
||||
"N", "S", "B", "NS", "SS", "BS", "L", "M", "NULL", "BOOL"
|
||||
};
|
||||
return types.contains(type);
|
||||
}
|
||||
|
||||
using function_handler_type = rjson::value(calculate_value_caller, const rjson::value*, const parsed::value::function_call&);
|
||||
static const
|
||||
std::unordered_map<std::string_view, function_handler_type*> function_handlers {
|
||||
{"list_append", [] (calculate_value_caller caller, const rjson::value* previous_item, const parsed::value::function_call& f) {
|
||||
if (caller != calculate_value_caller::UpdateExpression) {
|
||||
throw api_error::validation(
|
||||
format("{}: list_append() not allowed here", caller));
|
||||
}
|
||||
if (f._parameters.size() != 2) {
|
||||
throw api_error::validation(
|
||||
format("{}: list_append() accepts 2 parameters, got {}", caller, f._parameters.size()));
|
||||
}
|
||||
rjson::value v1 = calculate_value(f._parameters[0], caller, previous_item);
|
||||
rjson::value v2 = calculate_value(f._parameters[1], caller, previous_item);
|
||||
return list_concatenate(v1, v2);
|
||||
}
|
||||
},
|
||||
{"if_not_exists", [] (calculate_value_caller caller, const rjson::value* previous_item, const parsed::value::function_call& f) {
|
||||
if (caller != calculate_value_caller::UpdateExpression) {
|
||||
throw api_error::validation(
|
||||
format("{}: if_not_exists() not allowed here", caller));
|
||||
}
|
||||
if (f._parameters.size() != 2) {
|
||||
throw api_error::validation(
|
||||
format("{}: if_not_exists() accepts 2 parameters, got {}", caller, f._parameters.size()));
|
||||
}
|
||||
if (!std::holds_alternative<parsed::path>(f._parameters[0]._value)) {
|
||||
throw api_error::validation(
|
||||
format("{}: if_not_exists() must include path as its first argument", caller));
|
||||
}
|
||||
rjson::value v1 = calculate_value(f._parameters[0], caller, previous_item);
|
||||
rjson::value v2 = calculate_value(f._parameters[1], caller, previous_item);
|
||||
return v1.IsNull() ? std::move(v2) : std::move(v1);
|
||||
}
|
||||
},
|
||||
{"size", [] (calculate_value_caller caller, const rjson::value* previous_item, const parsed::value::function_call& f) {
|
||||
if (caller != calculate_value_caller::ConditionExpression) {
|
||||
throw api_error::validation(
|
||||
format("{}: size() not allowed here", caller));
|
||||
}
|
||||
if (f._parameters.size() != 1) {
|
||||
throw api_error::validation(
|
||||
format("{}: size() accepts 1 parameter, got {}", caller, f._parameters.size()));
|
||||
}
|
||||
rjson::value v = calculate_value(f._parameters[0], caller, previous_item);
|
||||
return calculate_size(v);
|
||||
}
|
||||
},
|
||||
{"attribute_exists", [] (calculate_value_caller caller, const rjson::value* previous_item, const parsed::value::function_call& f) {
|
||||
if (caller != calculate_value_caller::ConditionExpressionAlone) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_exists() not allowed here", caller));
|
||||
}
|
||||
if (f._parameters.size() != 1) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_exists() accepts 1 parameter, got {}", caller, f._parameters.size()));
|
||||
}
|
||||
if (!std::holds_alternative<parsed::path>(f._parameters[0]._value)) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_exists()'s parameter must be a path", caller));
|
||||
}
|
||||
rjson::value v = calculate_value(f._parameters[0], caller, previous_item);
|
||||
return to_bool_json(!v.IsNull());
|
||||
}
|
||||
},
|
||||
{"attribute_not_exists", [] (calculate_value_caller caller, const rjson::value* previous_item, const parsed::value::function_call& f) {
|
||||
if (caller != calculate_value_caller::ConditionExpressionAlone) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_not_exists() not allowed here", caller));
|
||||
}
|
||||
if (f._parameters.size() != 1) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_not_exists() accepts 1 parameter, got {}", caller, f._parameters.size()));
|
||||
}
|
||||
if (!std::holds_alternative<parsed::path>(f._parameters[0]._value)) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_not_exists()'s parameter must be a path", caller));
|
||||
}
|
||||
rjson::value v = calculate_value(f._parameters[0], caller, previous_item);
|
||||
return to_bool_json(v.IsNull());
|
||||
}
|
||||
},
|
||||
{"attribute_type", [] (calculate_value_caller caller, const rjson::value* previous_item, const parsed::value::function_call& f) {
|
||||
if (caller != calculate_value_caller::ConditionExpressionAlone) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_type() not allowed here", caller));
|
||||
}
|
||||
if (f._parameters.size() != 2) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_type() accepts 2 parameters, got {}", caller, f._parameters.size()));
|
||||
}
|
||||
// There is no real reason for the following check (not
|
||||
// allowing the type to come from a document attribute), but
|
||||
// DynamoDB does this check, so we do too...
|
||||
if (!f._parameters[1].is_constant()) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_types()'s first parameter must be an expression attribute", caller));
|
||||
}
|
||||
rjson::value v0 = calculate_value(f._parameters[0], caller, previous_item);
|
||||
rjson::value v1 = calculate_value(f._parameters[1], caller, previous_item);
|
||||
if (v1.IsObject() && v1.MemberCount() == 1 && v1.MemberBegin()->name == "S") {
|
||||
// If the type parameter is not one of the legal types
|
||||
// we should generate an error, not a failed condition:
|
||||
if (!known_type(rjson::to_string_view(v1.MemberBegin()->value))) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_types()'s second parameter, {}, is not a known type",
|
||||
caller, v1.MemberBegin()->value));
|
||||
}
|
||||
if (v0.IsObject() && v0.MemberCount() == 1) {
|
||||
return to_bool_json(v1.MemberBegin()->value == v0.MemberBegin()->name);
|
||||
} else {
|
||||
return to_bool_json(false);
|
||||
}
|
||||
} else {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_type() second parameter must refer to a string, got {}", caller, v1));
|
||||
}
|
||||
}
|
||||
},
|
||||
{"begins_with", [] (calculate_value_caller caller, const rjson::value* previous_item, const parsed::value::function_call& f) {
|
||||
if (caller != calculate_value_caller::ConditionExpressionAlone) {
|
||||
throw api_error::validation(
|
||||
format("{}: begins_with() not allowed here", caller));
|
||||
}
|
||||
if (f._parameters.size() != 2) {
|
||||
throw api_error::validation(
|
||||
format("{}: begins_with() accepts 2 parameters, got {}", caller, f._parameters.size()));
|
||||
}
|
||||
rjson::value v1 = calculate_value(f._parameters[0], caller, previous_item);
|
||||
rjson::value v2 = calculate_value(f._parameters[1], caller, previous_item);
|
||||
// TODO: There's duplication here with check_BEGINS_WITH().
|
||||
// But unfortunately, the two functions differ a bit.
|
||||
|
||||
// If one of v1 or v2 is malformed or has an unsupported type
|
||||
// (not B or S), what we do depends on whether it came from
|
||||
// the user's query (is_constant()), or the item. Unsupported
|
||||
// values in the query result in an error, but if they are in
|
||||
// the item, we silently return false (no match).
|
||||
bool bad = false;
|
||||
if (!v1.IsObject() || v1.MemberCount() != 1) {
|
||||
bad = true;
|
||||
if (f._parameters[0].is_constant()) {
|
||||
throw api_error::validation(format("{}: begins_with() encountered malformed AttributeValue: {}", caller, v1));
|
||||
}
|
||||
} else if (v1.MemberBegin()->name != "S" && v1.MemberBegin()->name != "B") {
|
||||
bad = true;
|
||||
if (f._parameters[0].is_constant()) {
|
||||
throw api_error::validation(format("{}: begins_with() supports only string or binary in AttributeValue: {}", caller, v1));
|
||||
}
|
||||
}
|
||||
if (!v2.IsObject() || v2.MemberCount() != 1) {
|
||||
bad = true;
|
||||
if (f._parameters[1].is_constant()) {
|
||||
throw api_error::validation(format("{}: begins_with() encountered malformed AttributeValue: {}", caller, v2));
|
||||
}
|
||||
} else if (v2.MemberBegin()->name != "S" && v2.MemberBegin()->name != "B") {
|
||||
bad = true;
|
||||
if (f._parameters[1].is_constant()) {
|
||||
throw api_error::validation(format("{}: begins_with() supports only string or binary in AttributeValue: {}", caller, v2));
|
||||
}
|
||||
}
|
||||
bool ret = false;
|
||||
if (!bad) {
|
||||
auto it1 = v1.MemberBegin();
|
||||
auto it2 = v2.MemberBegin();
|
||||
if (it1->name == it2->name) {
|
||||
if (it2->name == "S") {
|
||||
std::string_view val1 = rjson::to_string_view(it1->value);
|
||||
std::string_view val2 = rjson::to_string_view(it2->value);
|
||||
ret = val1.starts_with(val2);
|
||||
} else /* it2->name == "B" */ {
|
||||
ret = base64_begins_with(rjson::to_string_view(it1->value), rjson::to_string_view(it2->value));
|
||||
}
|
||||
}
|
||||
}
|
||||
return to_bool_json(ret);
|
||||
}
|
||||
},
|
||||
{"contains", [] (calculate_value_caller caller, const rjson::value* previous_item, const parsed::value::function_call& f) {
|
||||
if (caller != calculate_value_caller::ConditionExpressionAlone) {
|
||||
throw api_error::validation(
|
||||
format("{}: contains() not allowed here", caller));
|
||||
}
|
||||
if (f._parameters.size() != 2) {
|
||||
throw api_error::validation(
|
||||
format("{}: contains() accepts 2 parameters, got {}", caller, f._parameters.size()));
|
||||
}
|
||||
rjson::value v1 = calculate_value(f._parameters[0], caller, previous_item);
|
||||
rjson::value v2 = calculate_value(f._parameters[1], caller, previous_item);
|
||||
return to_bool_json(check_CONTAINS(v1.IsNull() ? nullptr : &v1, v2));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Given a parsed::value, which can refer either to a constant value from
|
||||
// ExpressionAttributeValues, to the value of some attribute, or to a function
|
||||
// of other values, this function calculates the resulting value.
|
||||
// "caller" determines which expression - ConditionExpression or
|
||||
// UpdateExpression - is asking for this value. We need to know this because
|
||||
// DynamoDB allows a different choice of functions for different expressions.
|
||||
rjson::value calculate_value(const parsed::value& v,
|
||||
calculate_value_caller caller,
|
||||
const rjson::value* previous_item) {
|
||||
return std::visit(overloaded_functor {
|
||||
[&] (const parsed::constant& c) -> rjson::value {
|
||||
return rjson::copy(calculate_value(c));
|
||||
},
|
||||
[&] (const parsed::value::function_call& f) -> rjson::value {
|
||||
auto function_it = function_handlers.find(std::string_view(f._function_name));
|
||||
if (function_it == function_handlers.end()) {
|
||||
throw api_error::validation(
|
||||
format("UpdateExpression: unknown function '{}' called.", f._function_name));
|
||||
}
|
||||
return function_it->second(caller, previous_item, f);
|
||||
},
|
||||
[&] (const parsed::path& p) -> rjson::value {
|
||||
if (!previous_item) {
|
||||
return rjson::null_value();
|
||||
}
|
||||
std::string update_path = p.root();
|
||||
if (p.has_operators()) {
|
||||
// FIXME: support this
|
||||
throw api_error::validation("Reading attribute paths not yet implemented");
|
||||
}
|
||||
const rjson::value* previous_value = rjson::find(*previous_item, update_path);
|
||||
return previous_value ? rjson::copy(*previous_value) : rjson::null_value();
|
||||
}
|
||||
}, v._value);
|
||||
}
|
||||
|
||||
// Same as calculate_value() above, except takes a set_rhs, which may be
|
||||
// either a single value, or v1+v2 or v1-v2.
|
||||
rjson::value calculate_value(const parsed::set_rhs& rhs,
|
||||
const rjson::value* previous_item) {
|
||||
switch (rhs._op) {
|
||||
case 'v':
|
||||
return calculate_value(rhs._v1, calculate_value_caller::UpdateExpression, previous_item);
|
||||
case '+': {
|
||||
rjson::value v1 = calculate_value(rhs._v1, calculate_value_caller::UpdateExpression, previous_item);
|
||||
rjson::value v2 = calculate_value(rhs._v2, calculate_value_caller::UpdateExpression, previous_item);
|
||||
return number_add(v1, v2);
|
||||
}
|
||||
case '-': {
|
||||
rjson::value v1 = calculate_value(rhs._v1, calculate_value_caller::UpdateExpression, previous_item);
|
||||
rjson::value v2 = calculate_value(rhs._v2, calculate_value_caller::UpdateExpression, previous_item);
|
||||
return number_subtract(v1, v2);
|
||||
}
|
||||
}
|
||||
// Can't happen
|
||||
return rjson::null_value();
|
||||
}
|
||||
|
||||
} // namespace alternator
|
||||
|
||||
@@ -145,12 +145,6 @@ REMOVE: R E M O V E;
|
||||
ADD: A D D;
|
||||
DELETE: D E L E T E;
|
||||
|
||||
AND: A N D;
|
||||
OR: O R;
|
||||
NOT: N O T;
|
||||
BETWEEN: B E T W E E N;
|
||||
IN: I N;
|
||||
|
||||
fragment ALPHA: 'A'..'Z' | 'a'..'z';
|
||||
fragment DIGIT: '0'..'9';
|
||||
fragment ALNUM: ALPHA | DIGIT | '_';
|
||||
@@ -171,19 +165,19 @@ path returns [parsed::path p]:
|
||||
| '[' INTEGER ']' { $p.add_index(std::stoi($INTEGER.text)); }
|
||||
)*;
|
||||
|
||||
value returns [parsed::value v]:
|
||||
VALREF { $v.set_valref($VALREF.text); }
|
||||
| path { $v.set_path($path.p); }
|
||||
| NAME { $v.set_func_name($NAME.text); }
|
||||
'(' x=value { $v.add_func_parameter($x.v); }
|
||||
(',' x=value { $v.add_func_parameter($x.v); })*
|
||||
update_expression_set_value returns [parsed::value v]:
|
||||
VALREF { $v.set_valref($VALREF.text); }
|
||||
| path { $v.set_path($path.p); }
|
||||
| NAME { $v.set_func_name($NAME.text); }
|
||||
'(' x=update_expression_set_value { $v.add_func_parameter($x.v); }
|
||||
(',' x=update_expression_set_value { $v.add_func_parameter($x.v); })*
|
||||
')'
|
||||
;
|
||||
|
||||
update_expression_set_rhs returns [parsed::set_rhs rhs]:
|
||||
v=value { $rhs.set_value(std::move($v.v)); }
|
||||
( '+' v=value { $rhs.set_plus(std::move($v.v)); }
|
||||
| '-' v=value { $rhs.set_minus(std::move($v.v)); }
|
||||
v=update_expression_set_value { $rhs.set_value(std::move($v.v)); }
|
||||
( '+' v=update_expression_set_value { $rhs.set_plus(std::move($v.v)); }
|
||||
| '-' v=update_expression_set_value { $rhs.set_minus(std::move($v.v)); }
|
||||
)?
|
||||
;
|
||||
|
||||
@@ -218,48 +212,3 @@ update_expression returns [parsed::update_expression e]:
|
||||
projection_expression returns [std::vector<parsed::path> v]:
|
||||
p=path { $v.push_back(std::move($p.p)); }
|
||||
(',' p=path { $v.push_back(std::move($p.p)); } )* EOF;
|
||||
|
||||
|
||||
primitive_condition returns [parsed::primitive_condition c]:
|
||||
v=value { $c.add_value(std::move($v.v));
|
||||
$c.set_operator(parsed::primitive_condition::type::VALUE); }
|
||||
( ( '=' { $c.set_operator(parsed::primitive_condition::type::EQ); }
|
||||
| '<' '>' { $c.set_operator(parsed::primitive_condition::type::NE); }
|
||||
| '<' { $c.set_operator(parsed::primitive_condition::type::LT); }
|
||||
| '<' '=' { $c.set_operator(parsed::primitive_condition::type::LE); }
|
||||
| '>' { $c.set_operator(parsed::primitive_condition::type::GT); }
|
||||
| '>' '=' { $c.set_operator(parsed::primitive_condition::type::GE); }
|
||||
)
|
||||
v=value { $c.add_value(std::move($v.v)); }
|
||||
| BETWEEN { $c.set_operator(parsed::primitive_condition::type::BETWEEN); }
|
||||
v=value { $c.add_value(std::move($v.v)); }
|
||||
AND
|
||||
v=value { $c.add_value(std::move($v.v)); }
|
||||
| IN '(' { $c.set_operator(parsed::primitive_condition::type::IN); }
|
||||
v=value { $c.add_value(std::move($v.v)); }
|
||||
(',' v=value { $c.add_value(std::move($v.v)); })*
|
||||
')'
|
||||
)?
|
||||
;
|
||||
|
||||
// The following rules for parsing boolean expressions are verbose and
|
||||
// somewhat strange because of Antlr 3's limitations on recursive rules,
|
||||
// common rule prefixes, and (lack of) support for operator precedence.
|
||||
// These rules could have been written more clearly using a more powerful
|
||||
// parser generator - such as Yacc.
|
||||
boolean_expression returns [parsed::condition_expression e]:
|
||||
b=boolean_expression_1 { $e.append(std::move($b.e), '|'); }
|
||||
(OR b=boolean_expression_1 { $e.append(std::move($b.e), '|'); } )*
|
||||
;
|
||||
boolean_expression_1 returns [parsed::condition_expression e]:
|
||||
b=boolean_expression_2 { $e.append(std::move($b.e), '&'); }
|
||||
(AND b=boolean_expression_2 { $e.append(std::move($b.e), '&'); } )*
|
||||
;
|
||||
boolean_expression_2 returns [parsed::condition_expression e]:
|
||||
p=primitive_condition { $e.set_primitive(std::move($p.c)); }
|
||||
| NOT b=boolean_expression_2 { $e = std::move($b.e); $e.apply_not(); }
|
||||
| '(' b=boolean_expression ')' { $e = std::move($b.e); }
|
||||
;
|
||||
|
||||
condition_expression returns [parsed::condition_expression e]:
|
||||
boolean_expression { e=std::move($boolean_expression.e); } EOF;
|
||||
|
||||
@@ -24,13 +24,8 @@
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
#include <string_view>
|
||||
|
||||
#include <seastar/util/noncopyable_function.hh>
|
||||
|
||||
#include "expressions_types.hh"
|
||||
#include "utils/rjson.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
@@ -41,62 +36,6 @@ public:
|
||||
|
||||
parsed::update_expression parse_update_expression(std::string query);
|
||||
std::vector<parsed::path> parse_projection_expression(std::string query);
|
||||
parsed::condition_expression parse_condition_expression(std::string query);
|
||||
|
||||
void resolve_update_expression(parsed::update_expression& ue,
|
||||
const rjson::value* expression_attribute_names,
|
||||
const rjson::value* expression_attribute_values,
|
||||
std::unordered_set<std::string>& used_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_values);
|
||||
void resolve_projection_expression(std::vector<parsed::path>& pe,
|
||||
const rjson::value* expression_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_names);
|
||||
void resolve_condition_expression(parsed::condition_expression& ce,
|
||||
const rjson::value* expression_attribute_names,
|
||||
const rjson::value* expression_attribute_values,
|
||||
std::unordered_set<std::string>& used_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_values);
|
||||
|
||||
void validate_value(const rjson::value& v, const char* caller);
|
||||
|
||||
bool condition_expression_on(const parsed::condition_expression& ce, std::string_view attribute);
|
||||
|
||||
// for_condition_expression_on() runs the given function on the attributes
|
||||
// that the expression uses. It may run for the same attribute more than once
|
||||
// if the same attribute is used more than once in the expression.
|
||||
void for_condition_expression_on(const parsed::condition_expression& ce, const noncopyable_function<void(std::string_view)>& func);
|
||||
|
||||
// calculate_value() behaves slightly different (especially, different
|
||||
// functions supported) when used in different types of expressions, as
|
||||
// enumerated in this enum:
|
||||
enum class calculate_value_caller {
|
||||
UpdateExpression, ConditionExpression, ConditionExpressionAlone
|
||||
};
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& out, calculate_value_caller caller) {
|
||||
switch (caller) {
|
||||
case calculate_value_caller::UpdateExpression:
|
||||
out << "UpdateExpression";
|
||||
break;
|
||||
case calculate_value_caller::ConditionExpression:
|
||||
out << "ConditionExpression";
|
||||
break;
|
||||
case calculate_value_caller::ConditionExpressionAlone:
|
||||
out << "ConditionExpression";
|
||||
break;
|
||||
default:
|
||||
out << "unknown type of expression";
|
||||
break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
rjson::value calculate_value(const parsed::value& v,
|
||||
calculate_value_caller caller,
|
||||
const rjson::value* previous_item);
|
||||
|
||||
rjson::value calculate_value(const parsed::set_rhs& rhs,
|
||||
const rjson::value* previous_item);
|
||||
|
||||
|
||||
} /* namespace alternator */
|
||||
|
||||
@@ -25,10 +25,6 @@
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
#include <seastar/core/shared_ptr.hh>
|
||||
|
||||
#include "utils/rjson.hh"
|
||||
|
||||
/*
|
||||
* Parsed representation of expressions and their components.
|
||||
*
|
||||
@@ -67,27 +63,10 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// When an expression is first parsed, all constants are references, like
|
||||
// ":val1", into ExpressionAttributeValues. This uses std::string() variant.
|
||||
// The resolve_value() function replaces these constants by the JSON item
|
||||
// extracted from the ExpressionAttributeValues.
|
||||
struct constant {
|
||||
// We use lw_shared_ptr<rjson::value> just to make rjson::value copyable,
|
||||
// to make this entire object copyable as ANTLR needs.
|
||||
using literal = lw_shared_ptr<rjson::value>;
|
||||
std::variant<std::string, literal> _value;
|
||||
void set(const rjson::value& v) {
|
||||
_value = make_lw_shared<rjson::value>(rjson::copy(v));
|
||||
}
|
||||
void set(std::string& s) {
|
||||
_value = s;
|
||||
}
|
||||
};
|
||||
|
||||
// "value" is is a value used in the right hand side of an assignment
|
||||
// expression, "SET a = ...". It can be a constant (a reference to a value
|
||||
// included in the request, e.g., ":val"), a path to an attribute from the
|
||||
// existing item (e.g., "a.b[3].c"), or a function of other such values.
|
||||
// expression, "SET a = ...". It can be a reference to a value included in
|
||||
// the request (":val"), a path to an attribute from the existing item
|
||||
// (e.g., "a.b[3].c"), or a function of other such values.
|
||||
// Note that the real right-hand-side of an assignment is actually a bit
|
||||
// more general - it allows either a value, or a value+value or value-value -
|
||||
// see class set_rhs below.
|
||||
@@ -96,12 +75,9 @@ struct value {
|
||||
std::string _function_name;
|
||||
std::vector<value> _parameters;
|
||||
};
|
||||
std::variant<constant, path, function_call> _value;
|
||||
void set_constant(constant c) {
|
||||
_value = std::move(c);
|
||||
}
|
||||
std::variant<std::string, path, function_call> _value;
|
||||
void set_valref(std::string s) {
|
||||
_value = constant { std::move(s) };
|
||||
_value = std::move(s);
|
||||
}
|
||||
void set_path(path p) {
|
||||
_value = std::move(p);
|
||||
@@ -112,15 +88,6 @@ struct value {
|
||||
void add_func_parameter(value v) {
|
||||
std::get<function_call>(_value)._parameters.emplace_back(std::move(v));
|
||||
}
|
||||
bool is_constant() const {
|
||||
return std::holds_alternative<constant>(_value);
|
||||
}
|
||||
bool is_path() const {
|
||||
return std::holds_alternative<path>(_value);
|
||||
}
|
||||
bool is_func() const {
|
||||
return std::holds_alternative<function_call>(_value);
|
||||
}
|
||||
};
|
||||
|
||||
// The right-hand-side of a SET in an update expression can be either a
|
||||
@@ -154,10 +121,10 @@ public:
|
||||
struct remove {
|
||||
};
|
||||
struct add {
|
||||
constant _valref;
|
||||
std::string _valref;
|
||||
};
|
||||
struct del {
|
||||
constant _valref;
|
||||
std::string _valref;
|
||||
};
|
||||
std::variant<set, remove, add, del> _action;
|
||||
|
||||
@@ -171,11 +138,11 @@ public:
|
||||
}
|
||||
void assign_add(path p, std::string v) {
|
||||
_path = std::move(p);
|
||||
_action = add { constant { std::move(v) } };
|
||||
_action = add { std::move(v) };
|
||||
}
|
||||
void assign_del(path p, std::string v) {
|
||||
_path = std::move(p);
|
||||
_action = del { constant { std::move(v) } };
|
||||
_action = del { std::move(v) };
|
||||
}
|
||||
};
|
||||
private:
|
||||
@@ -193,62 +160,6 @@ public:
|
||||
const std::vector<action>& actions() const {
|
||||
return _actions;
|
||||
}
|
||||
std::vector<action>& actions() {
|
||||
return _actions;
|
||||
}
|
||||
};
|
||||
|
||||
// A primitive_condition is a condition expression involving one condition,
|
||||
// while the full condition_expression below adds boolean logic over these
|
||||
// primitive conditions.
|
||||
// The supported primitive conditions are:
|
||||
// 1. Binary operators - v1 OP v2, where OP is =, <>, <, <=, >, or >= and
|
||||
// v1 and v2 are values - from the item (an attribute path), the query
|
||||
// (a ":val" reference), or a function of the the above (only the size()
|
||||
// function is supported).
|
||||
// 2. Ternary operator - v1 BETWEEN v2 and v3 (means v1 >= v2 AND v1 <= v3).
|
||||
// 3. N-ary operator - v1 IN ( v2, v3, ... )
|
||||
// 4. A single function call (attribute_exists etc.). The parser actually
|
||||
// accepts a more general "value" here but later stages reject a value
|
||||
// which is not a function call (because DynamoDB does it too).
|
||||
class primitive_condition {
|
||||
public:
|
||||
enum class type {
|
||||
UNDEFINED, VALUE, EQ, NE, LT, LE, GT, GE, BETWEEN, IN
|
||||
};
|
||||
type _op = type::UNDEFINED;
|
||||
std::vector<value> _values;
|
||||
void set_operator(type op) {
|
||||
_op = op;
|
||||
}
|
||||
void add_value(value&& v) {
|
||||
_values.push_back(std::move(v));
|
||||
}
|
||||
bool empty() const {
|
||||
return _op == type::UNDEFINED;
|
||||
}
|
||||
};
|
||||
|
||||
class condition_expression {
|
||||
public:
|
||||
bool _negated = false; // If true, the entire condition is negated
|
||||
struct condition_list {
|
||||
char op = '|'; // '&' or '|'
|
||||
std::vector<condition_expression> conditions;
|
||||
};
|
||||
std::variant<primitive_condition, condition_list> _expression = condition_list();
|
||||
|
||||
void set_primitive(primitive_condition&& p) {
|
||||
_expression = std::move(p);
|
||||
}
|
||||
void append(condition_expression&& c, char op);
|
||||
void apply_not() {
|
||||
_negated = !_negated;
|
||||
}
|
||||
bool empty() const {
|
||||
return std::holds_alternative<condition_list>(_expression) &&
|
||||
std::get<condition_list>(_expression).conditions.empty();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace parsed
|
||||
|
||||
172
alternator/rjson.cc
Normal file
172
alternator/rjson.cc
Normal file
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla 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 Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "rjson.hh"
|
||||
#include "error.hh"
|
||||
#include <seastar/core/print.hh>
|
||||
|
||||
namespace rjson {
|
||||
|
||||
static allocator the_allocator;
|
||||
|
||||
std::string print(const rjson::value& value) {
|
||||
string_buffer buffer;
|
||||
writer writer(buffer);
|
||||
value.Accept(writer);
|
||||
return std::string(buffer.GetString());
|
||||
}
|
||||
|
||||
rjson::value copy(const rjson::value& value) {
|
||||
return rjson::value(value, the_allocator);
|
||||
}
|
||||
|
||||
rjson::value parse(const std::string& str) {
|
||||
return parse_raw(str.c_str(), str.size());
|
||||
}
|
||||
|
||||
rjson::value parse_raw(const char* c_str, size_t size) {
|
||||
rjson::document d;
|
||||
d.Parse(c_str, size);
|
||||
if (d.HasParseError()) {
|
||||
throw rjson::error(format("Parsing JSON failed: {}", GetParseError_En(d.GetParseError())));
|
||||
}
|
||||
rjson::value& v = d;
|
||||
return std::move(v);
|
||||
}
|
||||
|
||||
rjson::value& get(rjson::value& value, rjson::string_ref_type name) {
|
||||
auto member_it = value.FindMember(name);
|
||||
if (member_it != value.MemberEnd())
|
||||
return member_it->value;
|
||||
else {
|
||||
throw rjson::error(format("JSON parameter {} not found", name));
|
||||
}
|
||||
}
|
||||
|
||||
const rjson::value& get(const rjson::value& value, rjson::string_ref_type name) {
|
||||
auto member_it = value.FindMember(name);
|
||||
if (member_it != value.MemberEnd())
|
||||
return member_it->value;
|
||||
else {
|
||||
throw rjson::error(format("JSON parameter {} not found", name));
|
||||
}
|
||||
}
|
||||
|
||||
rjson::value from_string(const std::string& str) {
|
||||
return rjson::value(str.c_str(), str.size(), the_allocator);
|
||||
}
|
||||
|
||||
rjson::value from_string(const sstring& str) {
|
||||
return rjson::value(str.c_str(), str.size(), the_allocator);
|
||||
}
|
||||
|
||||
rjson::value from_string(const char* str, size_t size) {
|
||||
return rjson::value(str, size, the_allocator);
|
||||
}
|
||||
|
||||
const rjson::value* find(const rjson::value& value, string_ref_type name) {
|
||||
auto member_it = value.FindMember(name);
|
||||
return member_it != value.MemberEnd() ? &member_it->value : nullptr;
|
||||
}
|
||||
|
||||
rjson::value* find(rjson::value& value, string_ref_type name) {
|
||||
auto member_it = value.FindMember(name);
|
||||
return member_it != value.MemberEnd() ? &member_it->value : nullptr;
|
||||
}
|
||||
|
||||
void set_with_string_name(rjson::value& base, const std::string& name, rjson::value&& member) {
|
||||
base.AddMember(rjson::value(name.c_str(), name.size(), the_allocator), std::move(member), the_allocator);
|
||||
}
|
||||
|
||||
void set_with_string_name(rjson::value& base, const std::string& name, rjson::string_ref_type member) {
|
||||
base.AddMember(rjson::value(name.c_str(), name.size(), the_allocator), rjson::value(member), the_allocator);
|
||||
}
|
||||
|
||||
void set(rjson::value& base, rjson::string_ref_type name, rjson::value&& member) {
|
||||
base.AddMember(name, std::move(member), the_allocator);
|
||||
}
|
||||
|
||||
void set(rjson::value& base, rjson::string_ref_type name, rjson::string_ref_type member) {
|
||||
base.AddMember(name, rjson::value(member), the_allocator);
|
||||
}
|
||||
|
||||
void push_back(rjson::value& base_array, rjson::value&& item) {
|
||||
base_array.PushBack(std::move(item), the_allocator);
|
||||
|
||||
}
|
||||
|
||||
bool single_value_comp::operator()(const rjson::value& r1, const rjson::value& r2) const {
|
||||
auto r1_type = r1.GetType();
|
||||
auto r2_type = r2.GetType();
|
||||
|
||||
// null is the smallest type and compares with every other type, nothing is lesser than null
|
||||
if (r1_type == rjson::type::kNullType || r2_type == rjson::type::kNullType) {
|
||||
return r1_type < r2_type;
|
||||
}
|
||||
// only null, true, and false are comparable with each other, other types are not compatible
|
||||
if (r1_type != r2_type) {
|
||||
if (r1_type > rjson::type::kTrueType || r2_type > rjson::type::kTrueType) {
|
||||
throw rjson::error(format("Types are not comparable: {} {}", r1, r2));
|
||||
}
|
||||
}
|
||||
|
||||
switch (r1_type) {
|
||||
case rjson::type::kNullType:
|
||||
// fall-through
|
||||
case rjson::type::kFalseType:
|
||||
// fall-through
|
||||
case rjson::type::kTrueType:
|
||||
return r1_type < r2_type;
|
||||
case rjson::type::kObjectType:
|
||||
throw rjson::error("Object type comparison is not supported");
|
||||
case rjson::type::kArrayType:
|
||||
throw rjson::error("Array type comparison is not supported");
|
||||
case rjson::type::kStringType: {
|
||||
const size_t r1_len = r1.GetStringLength();
|
||||
const size_t r2_len = r2.GetStringLength();
|
||||
size_t len = std::min(r1_len, r2_len);
|
||||
int result = std::strncmp(r1.GetString(), r2.GetString(), len);
|
||||
return result < 0 || (result == 0 && r1_len < r2_len);
|
||||
}
|
||||
case rjson::type::kNumberType: {
|
||||
if (r1.IsInt() && r2.IsInt()) {
|
||||
return r1.GetInt() < r2.GetInt();
|
||||
} else if (r1.IsUint() && r2.IsUint()) {
|
||||
return r1.GetUint() < r2.GetUint();
|
||||
} else if (r1.IsInt64() && r2.IsInt64()) {
|
||||
return r1.GetInt64() < r2.GetInt64();
|
||||
} else if (r1.IsUint64() && r2.IsUint64()) {
|
||||
return r1.GetUint64() < r2.GetUint64();
|
||||
} else {
|
||||
// it's safe to call GetDouble() on any number type
|
||||
return r1.GetDouble() < r2.GetDouble();
|
||||
}
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // end namespace rjson
|
||||
|
||||
std::ostream& std::operator<<(std::ostream& os, const rjson::value& v) {
|
||||
return os << rjson::print(v);
|
||||
}
|
||||
@@ -80,21 +80,6 @@ using string_buffer = rapidjson::GenericStringBuffer<encoding>;
|
||||
using writer = rapidjson::Writer<string_buffer, encoding>;
|
||||
using type = rapidjson::Type;
|
||||
|
||||
/**
|
||||
* exception specializations.
|
||||
*/
|
||||
class malformed_value : public error {
|
||||
public:
|
||||
malformed_value(std::string_view name, const rjson::value& value);
|
||||
malformed_value(std::string_view name, std::string_view value);
|
||||
};
|
||||
|
||||
class missing_value : public error {
|
||||
public:
|
||||
missing_value(std::string_view name);
|
||||
};
|
||||
|
||||
|
||||
// Returns an object representing JSON's null
|
||||
inline rjson::value null_value() {
|
||||
return rjson::value(rapidjson::kNullType);
|
||||
@@ -119,81 +104,38 @@ inline rjson::value empty_string() {
|
||||
// The representation is dense - without any redundant indentation.
|
||||
std::string print(const rjson::value& value);
|
||||
|
||||
// Returns a string_view to the string held in a JSON value (which is
|
||||
// assumed to hold a string, i.e., v.IsString() == true). This is a view
|
||||
// to the existing data - no copying is done.
|
||||
inline std::string_view to_string_view(const rjson::value& v) {
|
||||
return std::string_view(v.GetString(), v.GetStringLength());
|
||||
}
|
||||
|
||||
// Copies given JSON value - involves allocation
|
||||
rjson::value copy(const rjson::value& value);
|
||||
|
||||
// Parses a JSON value from given string or raw character array.
|
||||
// The string/char array liveness does not need to be persisted,
|
||||
// as parse() will allocate member names and values.
|
||||
// as both parse() and parse_raw() will allocate member names and values.
|
||||
// Throws rjson::error if parsing failed.
|
||||
rjson::value parse(std::string_view str);
|
||||
// Parses a JSON value returns a disengaged optional on failure.
|
||||
// NOTICE: any error context will be lost, so this function should
|
||||
// be used only if one does not care why parsing failed.
|
||||
std::optional<rjson::value> try_parse(std::string_view str);
|
||||
// Needs to be run in thread context
|
||||
rjson::value parse_yieldable(std::string_view str);
|
||||
rjson::value parse(const std::string& str);
|
||||
rjson::value parse_raw(const char* c_str, size_t size);
|
||||
|
||||
// Creates a JSON value (of JSON string type) out of internal string representations.
|
||||
// The string value is copied, so str's liveness does not need to be persisted.
|
||||
rjson::value from_string(const std::string& str);
|
||||
rjson::value from_string(const sstring& str);
|
||||
rjson::value from_string(const char* str, size_t size);
|
||||
rjson::value from_string(std::string_view view);
|
||||
|
||||
// Returns a pointer to JSON member if it exists, nullptr otherwise
|
||||
rjson::value* find(rjson::value& value, std::string_view name);
|
||||
const rjson::value* find(const rjson::value& value, std::string_view name);
|
||||
rjson::value* find(rjson::value& value, rjson::string_ref_type name);
|
||||
const rjson::value* find(const rjson::value& value, rjson::string_ref_type name);
|
||||
|
||||
// Returns a reference to JSON member if it exists, throws otherwise
|
||||
rjson::value& get(rjson::value& value, std::string_view name);
|
||||
const rjson::value& get(const rjson::value& value, std::string_view name);
|
||||
|
||||
/**
|
||||
* Type conversion getter.
|
||||
* Will typically require an existing rapidjson::internal::TypeHelper<...>
|
||||
* to exist for the type queried.
|
||||
*/
|
||||
template<typename T>
|
||||
T get(const rjson::value& value, std::string_view name) {
|
||||
auto& v = get(value, name);
|
||||
try {
|
||||
return v.Get<T>();
|
||||
} catch (...) {
|
||||
std::throw_with_nested(malformed_value(name, v));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type conversion opt getter.
|
||||
* Will typically require an existing rapidjson::internal::TypeHelper<...>
|
||||
* to exist for the type queried.
|
||||
*
|
||||
* Return std::nullopt if value does not exist.
|
||||
*/
|
||||
template<typename T>
|
||||
std::optional<T> get_opt(const rjson::value& value, std::string_view name) {
|
||||
auto* v = find(value, name);
|
||||
try {
|
||||
return v ? std::optional<T>(v->Get<T>()) : std::nullopt;
|
||||
} catch (...) {
|
||||
std::throw_with_nested(malformed_value(name, *v));
|
||||
}
|
||||
}
|
||||
rjson::value& get(rjson::value& value, rjson::string_ref_type name);
|
||||
const rjson::value& get(const rjson::value& value, rjson::string_ref_type name);
|
||||
|
||||
// Sets a member in given JSON object by moving the member - allocates the name.
|
||||
// Throws if base is not a JSON object.
|
||||
void set_with_string_name(rjson::value& base, std::string_view name, rjson::value&& member);
|
||||
void set_with_string_name(rjson::value& base, const std::string& name, rjson::value&& member);
|
||||
|
||||
// Sets a string member in given JSON object by assigning its reference - allocates the name.
|
||||
// NOTICE: member string liveness must be ensured to be at least as long as base's.
|
||||
// Throws if base is not a JSON object.
|
||||
void set_with_string_name(rjson::value& base, std::string_view name, rjson::string_ref_type member);
|
||||
void set_with_string_name(rjson::value& base, const std::string& name, rjson::string_ref_type member);
|
||||
|
||||
// Sets a member in given JSON object by moving the member.
|
||||
// NOTICE: name liveness must be ensured to be at least as long as base's.
|
||||
@@ -206,67 +148,14 @@ void set(rjson::value& base, rjson::string_ref_type name, rjson::value&& member)
|
||||
// Throws if base is not a JSON object.
|
||||
void set(rjson::value& base, rjson::string_ref_type name, rjson::string_ref_type member);
|
||||
|
||||
/**
|
||||
* Type conversion setter.
|
||||
* Will typically require an existing rapidjson::internal::TypeHelper<...>
|
||||
* to exist for the type written.
|
||||
*
|
||||
* (Note: order is important. Need to be after set(..., rjson::value),
|
||||
* otherwise the enable_if must be expanded)
|
||||
*/
|
||||
template<typename T>
|
||||
std::enable_if_t<!std::is_constructible_v<string_ref_type, T>>
|
||||
set(rjson::value& base, rjson::string_ref_type name, T&& member) {
|
||||
extern allocator the_allocator;
|
||||
|
||||
rjson::value v;
|
||||
v.Set(std::forward<T>(member), the_allocator);
|
||||
set(base, std::move(name), std::move(v));
|
||||
}
|
||||
|
||||
// Adds a value to a JSON list by moving the item to its end.
|
||||
// Throws if base_array is not a JSON array.
|
||||
void push_back(rjson::value& base_array, rjson::value&& item);
|
||||
|
||||
// Remove a member from a JSON object. Throws if value isn't an object.
|
||||
bool remove_member(rjson::value& value, std::string_view name);
|
||||
|
||||
struct single_value_comp {
|
||||
bool operator()(const rjson::value& r1, const rjson::value& r2) const;
|
||||
};
|
||||
|
||||
// Helper function for parsing a JSON straight into a map
|
||||
// of strings representing their values - useful for various
|
||||
// database helper functions.
|
||||
// This function exists for historical reasons - existing infrastructure
|
||||
// relies on being able to transform a JSON string into a map of sstrings.
|
||||
template<typename Map>
|
||||
requires (std::is_same_v<Map, std::map<sstring, sstring>> || std::is_same_v<Map, std::unordered_map<sstring, sstring>>)
|
||||
Map parse_to_map(std::string_view raw) {
|
||||
Map map;
|
||||
rjson::value root = rjson::parse(raw);
|
||||
if (root.IsNull()) {
|
||||
return map;
|
||||
}
|
||||
if (!root.IsObject()) {
|
||||
throw rjson::error("Only json objects can be transformed to maps. Encountered: " + std::string(raw));
|
||||
}
|
||||
for (auto it = root.MemberBegin(); it != root.MemberEnd(); ++it) {
|
||||
if (it->value.IsString()) {
|
||||
map.emplace(sstring(rjson::to_string_view(it->name)), sstring(rjson::to_string_view(it->value)));
|
||||
} else {
|
||||
map.emplace(sstring(rjson::to_string_view(it->name)), sstring(rjson::print(it->value)));
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// This function exists for historical reasons as well.
|
||||
rjson::value from_string_map(const std::map<sstring, sstring>& map);
|
||||
|
||||
// The function operates on sstrings for historical reasons.
|
||||
sstring quote_json_string(const sstring& value);
|
||||
|
||||
} // end namespace rjson
|
||||
|
||||
namespace std {
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla 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 Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "seastarx.hh"
|
||||
#include "service/storage_proxy.hh"
|
||||
#include "service/storage_proxy.hh"
|
||||
#include "utils/rjson.hh"
|
||||
#include "executor.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
// An rmw_operation encapsulates the common logic of all the item update
|
||||
// operations which may involve a read of the item before the write
|
||||
// (so-called Read-Modify-Write operations). These operations include PutItem,
|
||||
// UpdateItem and DeleteItem: All of these may be conditional operations (the
|
||||
// "Expected" parameter) which requir a read before the write, and UpdateItem
|
||||
// may also have an update expression which refers to the item's old value.
|
||||
//
|
||||
// The code below supports running the read and the write together as one
|
||||
// transaction using LWT (this is why rmw_operation is a subclass of
|
||||
// cas_request, as required by storage_proxy::cas()), but also has optional
|
||||
// modes not using LWT.
|
||||
class rmw_operation : public service::cas_request, public enable_shared_from_this<rmw_operation> {
|
||||
public:
|
||||
// The following options choose which mechanism to use for isolating
|
||||
// parallel write operations:
|
||||
// * The FORBID_RMW option forbids RMW (read-modify-write) operations
|
||||
// such as conditional updates. For the remaining write-only
|
||||
// operations, ordinary quorum writes are isolated enough.
|
||||
// * The LWT_ALWAYS option always uses LWT (lightweight transactions)
|
||||
// for any write operation - whether or not it also has a read.
|
||||
// * The LWT_RMW_ONLY option uses LWT only for RMW operations, and uses
|
||||
// ordinary quorum writes for write-only operations.
|
||||
// This option is not safe if the user may send both RMW and write-only
|
||||
// operations on the same item.
|
||||
// * The UNSAFE_RMW option does read-modify-write operations as separate
|
||||
// read and write. It is unsafe - concurrent RMW operations are not
|
||||
// isolated at all. This option will likely be removed in the future.
|
||||
enum class write_isolation {
|
||||
FORBID_RMW, LWT_ALWAYS, LWT_RMW_ONLY, UNSAFE_RMW
|
||||
};
|
||||
static constexpr auto WRITE_ISOLATION_TAG_KEY = "system:write_isolation";
|
||||
|
||||
static write_isolation get_write_isolation_for_schema(schema_ptr schema);
|
||||
|
||||
static write_isolation default_write_isolation;
|
||||
public:
|
||||
static void set_default_write_isolation(std::string_view mode);
|
||||
|
||||
protected:
|
||||
// The full request JSON
|
||||
rjson::value _request;
|
||||
// All RMW operations involve a single item with a specific partition
|
||||
// and optional clustering key, in a single table, so the following
|
||||
// information is common to all of them:
|
||||
schema_ptr _schema;
|
||||
partition_key _pk = partition_key::make_empty();
|
||||
clustering_key _ck = clustering_key::make_empty();
|
||||
write_isolation _write_isolation;
|
||||
|
||||
// All RMW operations can have a ReturnValues parameter from the following
|
||||
// choices. But note that only UpdateItem actually supports all of them:
|
||||
enum class returnvalues {
|
||||
NONE, ALL_OLD, UPDATED_OLD, ALL_NEW, UPDATED_NEW
|
||||
} _returnvalues;
|
||||
static returnvalues parse_returnvalues(const rjson::value& request);
|
||||
// When _returnvalues != NONE, apply() should store here, in JSON form,
|
||||
// the values which are to be returned in the "Attributes" field.
|
||||
// The default null JSON means do not return an Attributes field at all.
|
||||
// This field is marked "mutable" so that the const apply() can modify
|
||||
// it (see explanation below), but note that because apply() may be
|
||||
// called more than once, if apply() will sometimes set this field it
|
||||
// must set it (even if just to the default empty value) every time.
|
||||
mutable rjson::value _return_attributes;
|
||||
public:
|
||||
// The constructor of a rmw_operation subclass should parse the request
|
||||
// and try to discover as many input errors as it can before really
|
||||
// attempting the read or write operations.
|
||||
rmw_operation(service::storage_proxy& proxy, rjson::value&& request);
|
||||
// rmw_operation subclasses (update_item_operation, put_item_operation
|
||||
// and delete_item_operation) shall implement an apply() function which
|
||||
// takes the previous value of the item (if it was read) and creates the
|
||||
// write mutation. If the previous value of item does not pass the needed
|
||||
// conditional expression, apply() should return an empty optional.
|
||||
// apply() may throw if it encounters input errors not discovered during
|
||||
// the constructor.
|
||||
// apply() may be called more than once in case of contention, so it must
|
||||
// not change the state saved in the object (issue #7218 was caused by
|
||||
// violating this). We mark apply() "const" to let the compiler validate
|
||||
// this for us. The output-only field _return_attributes is marked
|
||||
// "mutable" above so that apply() can still write to it.
|
||||
virtual std::optional<mutation> apply(std::unique_ptr<rjson::value> previous_item, api::timestamp_type ts) const = 0;
|
||||
// Convert the above apply() into the signature needed by cas_request:
|
||||
virtual std::optional<mutation> apply(foreign_ptr<lw_shared_ptr<query::result>> qr, const query::partition_slice& slice, api::timestamp_type ts) override;
|
||||
virtual ~rmw_operation() = default;
|
||||
schema_ptr schema() const { return _schema; }
|
||||
const rjson::value& request() const { return _request; }
|
||||
rjson::value&& move_request() && { return std::move(_request); }
|
||||
future<executor::request_return_type> execute(service::storage_proxy& proxy,
|
||||
service::client_state& client_state,
|
||||
tracing::trace_state_ptr trace_state,
|
||||
service_permit permit,
|
||||
bool needs_read_before_write,
|
||||
stats& stats);
|
||||
std::optional<shard_id> shard_for_execute(bool needs_read_before_write);
|
||||
};
|
||||
|
||||
} // namespace alternator
|
||||
@@ -31,8 +31,8 @@ static logging::logger slogger("alternator-serialization");
|
||||
|
||||
namespace alternator {
|
||||
|
||||
type_info type_info_from_string(std::string_view type) {
|
||||
static thread_local const std::unordered_map<std::string_view, type_info> type_infos = {
|
||||
type_info type_info_from_string(std::string type) {
|
||||
static thread_local const std::unordered_map<std::string, type_info> type_infos = {
|
||||
{"S", {alternator_type::S, utf8_type}},
|
||||
{"B", {alternator_type::B, bytes_type}},
|
||||
{"BOOL", {alternator_type::BOOL, boolean_type}},
|
||||
@@ -65,7 +65,7 @@ struct from_json_visitor {
|
||||
|
||||
void operator()(const reversed_type_impl& t) const { visit(*t.underlying_type(), from_json_visitor{v, bo}); };
|
||||
void operator()(const string_type_impl& t) {
|
||||
bo.write(t.from_string(rjson::to_string_view(v)));
|
||||
bo.write(t.from_string(sstring_view(v.GetString(), v.GetStringLength())));
|
||||
}
|
||||
void operator()(const bytes_type_impl& t) const {
|
||||
bo.write(base64_decode(v));
|
||||
@@ -74,27 +74,23 @@ struct from_json_visitor {
|
||||
bo.write(boolean_type->decompose(v.GetBool()));
|
||||
}
|
||||
void operator()(const decimal_type_impl& t) const {
|
||||
try {
|
||||
bo.write(t.from_string(rjson::to_string_view(v)));
|
||||
} catch (const marshal_exception& e) {
|
||||
throw api_error::validation(format("The parameter cannot be converted to a numeric value: {}", v));
|
||||
}
|
||||
bo.write(t.from_string(sstring_view(v.GetString(), v.GetStringLength())));
|
||||
}
|
||||
// default
|
||||
void operator()(const abstract_type& t) const {
|
||||
bo.write(from_json_object(t, v, cql_serialization_format::internal()));
|
||||
bo.write(from_json_object(t, Json::Value(rjson::print(v)), cql_serialization_format::internal()));
|
||||
}
|
||||
};
|
||||
|
||||
bytes serialize_item(const rjson::value& item) {
|
||||
if (item.IsNull() || item.MemberCount() != 1) {
|
||||
throw api_error::validation(format("An item can contain only one attribute definition: {}", item));
|
||||
throw api_error("ValidationException", format("An item can contain only one attribute definition: {}", item));
|
||||
}
|
||||
auto it = item.MemberBegin();
|
||||
type_info type_info = type_info_from_string(rjson::to_string_view(it->name)); // JSON keys are guaranteed to be strings
|
||||
type_info type_info = type_info_from_string(it->name.GetString()); // JSON keys are guaranteed to be strings
|
||||
|
||||
if (type_info.atype == alternator_type::NOT_SUPPORTED_YET) {
|
||||
slogger.trace("Non-optimal serialization of type {}", it->name);
|
||||
slogger.trace("Non-optimal serialization of type {}", it->name.GetString());
|
||||
return bytes{int8_t(type_info.atype)} + to_bytes(rjson::print(item));
|
||||
}
|
||||
|
||||
@@ -125,14 +121,14 @@ struct to_json_visitor {
|
||||
}
|
||||
// default
|
||||
void operator()(const abstract_type& t) const {
|
||||
rjson::set_with_string_name(deserialized, type_ident, rjson::parse(to_json_string(t, bytes(bv))));
|
||||
rjson::set_with_string_name(deserialized, type_ident, rjson::parse(t.to_string(bytes(bv))));
|
||||
}
|
||||
};
|
||||
|
||||
rjson::value deserialize_item(bytes_view bv) {
|
||||
rjson::value deserialized(rapidjson::kObjectType);
|
||||
if (bv.empty()) {
|
||||
throw api_error::validation("Serialized value empty");
|
||||
throw api_error("ValidationException", "Serialized value empty");
|
||||
}
|
||||
|
||||
alternator_type atype = alternator_type(bv[0]);
|
||||
@@ -140,7 +136,7 @@ rjson::value deserialize_item(bytes_view bv) {
|
||||
|
||||
if (atype == alternator_type::NOT_SUPPORTED_YET) {
|
||||
slogger.trace("Non-optimal deserialization of alternator type {}", int8_t(atype));
|
||||
return rjson::parse(std::string_view(reinterpret_cast<const char *>(bv.data()), bv.size()));
|
||||
return rjson::parse_raw(reinterpret_cast<const char *>(bv.data()), bv.size());
|
||||
}
|
||||
type_representation type_representation = represent_type(atype);
|
||||
visit(*type_representation.dtype, to_json_visitor{deserialized, type_representation.ident, bv});
|
||||
@@ -157,48 +153,34 @@ std::string type_to_string(data_type type) {
|
||||
};
|
||||
auto it = types.find(type);
|
||||
if (it == types.end()) {
|
||||
// fall back to string, in order to be able to present
|
||||
// internal Scylla types in a human-readable way
|
||||
return "S";
|
||||
throw std::runtime_error(format("Unknown type {}", type->name()));
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
bytes get_key_column_value(const rjson::value& item, const column_definition& column) {
|
||||
std::string column_name = column.name_as_text();
|
||||
const rjson::value* key_typed_value = rjson::find(item, column_name);
|
||||
if (!key_typed_value) {
|
||||
throw api_error::validation(format("Key column {} not found", column_name));
|
||||
std::string expected_type = type_to_string(column.type);
|
||||
|
||||
const rjson::value& key_typed_value = rjson::get(item, rjson::value::StringRefType(column_name.c_str()));
|
||||
if (!key_typed_value.IsObject() || key_typed_value.MemberCount() != 1) {
|
||||
throw api_error("ValidationException",
|
||||
format("Missing or invalid value object for key column {}: {}", column_name, item));
|
||||
}
|
||||
return get_key_from_typed_value(*key_typed_value, column);
|
||||
return get_key_from_typed_value(key_typed_value, column, expected_type);
|
||||
}
|
||||
|
||||
// Parses the JSON encoding for a key value, which is a map with a single
|
||||
// entry, whose key is the type (expected to match the key column's type)
|
||||
// and the value is the encoded value.
|
||||
bytes get_key_from_typed_value(const rjson::value& key_typed_value, const column_definition& column) {
|
||||
if (!key_typed_value.IsObject() || key_typed_value.MemberCount() != 1 ||
|
||||
!key_typed_value.MemberBegin()->value.IsString()) {
|
||||
throw api_error::validation(
|
||||
format("Malformed value object for key column {}: {}",
|
||||
column.name_as_text(), key_typed_value));
|
||||
}
|
||||
|
||||
bytes get_key_from_typed_value(const rjson::value& key_typed_value, const column_definition& column, const std::string& expected_type) {
|
||||
auto it = key_typed_value.MemberBegin();
|
||||
if (it->name != type_to_string(column.type)) {
|
||||
throw api_error::validation(
|
||||
if (it->name.GetString() != expected_type) {
|
||||
throw api_error("ValidationException",
|
||||
format("Type mismatch: expected type {} for key column {}, got type {}",
|
||||
type_to_string(column.type), column.name_as_text(), it->name));
|
||||
}
|
||||
std::string_view value_view = rjson::to_string_view(it->value);
|
||||
if (value_view.empty()) {
|
||||
throw api_error::validation(
|
||||
format("The AttributeValue for a key attribute cannot contain an empty string value. Key: {}", column.name_as_text()));
|
||||
expected_type, column.name_as_text(), it->name.GetString()));
|
||||
}
|
||||
if (column.type == bytes_type) {
|
||||
return base64_decode(it->value);
|
||||
} else {
|
||||
return column.type->from_string(rjson::to_string_view(it->value));
|
||||
return column.type->from_string(it->value.GetString());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -216,11 +198,8 @@ rjson::value json_key_column_value(bytes_view cell, const column_definition& col
|
||||
auto s = to_json_string(*decimal_type, bytes(cell));
|
||||
return rjson::from_string(s);
|
||||
} else {
|
||||
// Support for arbitrary key types is useful for parsing values of virtual tables,
|
||||
// which can involve any type supported by Scylla.
|
||||
// In order to guarantee that the returned type is parsable by alternator clients,
|
||||
// they are represented simply as strings.
|
||||
return rjson::from_string(column.type->to_string(bytes(cell)));
|
||||
// We shouldn't get here, we shouldn't see such key columns.
|
||||
throw std::runtime_error(format("Unexpected key type: {}", column.type->name()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,24 +230,20 @@ clustering_key ck_from_json(const rjson::value& item, schema_ptr schema) {
|
||||
|
||||
big_decimal unwrap_number(const rjson::value& v, std::string_view diagnostic) {
|
||||
if (!v.IsObject() || v.MemberCount() != 1) {
|
||||
throw api_error::validation(format("{}: invalid number object", diagnostic));
|
||||
throw api_error("ValidationException", format("{}: invalid number object", diagnostic));
|
||||
}
|
||||
auto it = v.MemberBegin();
|
||||
if (it->name != "N") {
|
||||
throw api_error::validation(format("{}: expected number, found type '{}'", diagnostic, it->name));
|
||||
throw api_error("ValidationException", format("{}: expected number, found type '{}'", diagnostic, it->name));
|
||||
}
|
||||
try {
|
||||
if (it->value.IsNumber()) {
|
||||
// FIXME(sarna): should use big_decimal constructor with numeric values directly:
|
||||
return big_decimal(rjson::print(it->value));
|
||||
}
|
||||
if (!it->value.IsString()) {
|
||||
throw api_error::validation(format("{}: improperly formatted number constant", diagnostic));
|
||||
}
|
||||
return big_decimal(rjson::to_string_view(it->value));
|
||||
} catch (const marshal_exception& e) {
|
||||
throw api_error::validation(format("The parameter cannot be converted to a numeric value: {}", it->value));
|
||||
if (it->value.IsNumber()) {
|
||||
// FIXME(sarna): should use big_decimal constructor with numeric values directly:
|
||||
return big_decimal(rjson::print(it->value));
|
||||
}
|
||||
if (!it->value.IsString()) {
|
||||
throw api_error("ValidationException", format("{}: improperly formatted number constant", diagnostic));
|
||||
}
|
||||
return big_decimal(it->value.GetString());
|
||||
}
|
||||
|
||||
const std::pair<std::string, const rjson::value*> unwrap_set(const rjson::value& v) {
|
||||
@@ -283,93 +258,4 @@ const std::pair<std::string, const rjson::value*> unwrap_set(const rjson::value&
|
||||
return std::make_pair(it_key, &(it->value));
|
||||
}
|
||||
|
||||
const rjson::value* unwrap_list(const rjson::value& v) {
|
||||
if (!v.IsObject() || v.MemberCount() != 1) {
|
||||
return nullptr;
|
||||
}
|
||||
auto it = v.MemberBegin();
|
||||
if (it->name != std::string("L")) {
|
||||
return nullptr;
|
||||
}
|
||||
return &(it->value);
|
||||
}
|
||||
|
||||
// Take two JSON-encoded numeric values ({"N": "thenumber"}) and return the
|
||||
// sum, again as a JSON-encoded number.
|
||||
rjson::value number_add(const rjson::value& v1, const rjson::value& v2) {
|
||||
auto n1 = unwrap_number(v1, "UpdateExpression");
|
||||
auto n2 = unwrap_number(v2, "UpdateExpression");
|
||||
rjson::value ret = rjson::empty_object();
|
||||
std::string str_ret = std::string((n1 + n2).to_string());
|
||||
rjson::set(ret, "N", rjson::from_string(str_ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
rjson::value number_subtract(const rjson::value& v1, const rjson::value& v2) {
|
||||
auto n1 = unwrap_number(v1, "UpdateExpression");
|
||||
auto n2 = unwrap_number(v2, "UpdateExpression");
|
||||
rjson::value ret = rjson::empty_object();
|
||||
std::string str_ret = std::string((n1 - n2).to_string());
|
||||
rjson::set(ret, "N", rjson::from_string(str_ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Take two JSON-encoded set values (e.g. {"SS": [...the actual set]}) and
|
||||
// return the sum of both sets, again as a set value.
|
||||
rjson::value set_sum(const rjson::value& v1, const rjson::value& v2) {
|
||||
auto [set1_type, set1] = unwrap_set(v1);
|
||||
auto [set2_type, set2] = unwrap_set(v2);
|
||||
if (set1_type != set2_type) {
|
||||
throw api_error::validation(format("Mismatched set types: {} and {}", set1_type, set2_type));
|
||||
}
|
||||
if (!set1 || !set2) {
|
||||
throw api_error::validation("UpdateExpression: ADD operation for sets must be given sets as arguments");
|
||||
}
|
||||
rjson::value sum = rjson::copy(*set1);
|
||||
std::set<rjson::value, rjson::single_value_comp> set1_raw;
|
||||
for (auto it = sum.Begin(); it != sum.End(); ++it) {
|
||||
set1_raw.insert(rjson::copy(*it));
|
||||
}
|
||||
for (const auto& a : set2->GetArray()) {
|
||||
if (!set1_raw.contains(a)) {
|
||||
rjson::push_back(sum, rjson::copy(a));
|
||||
}
|
||||
}
|
||||
rjson::value ret = rjson::empty_object();
|
||||
rjson::set_with_string_name(ret, set1_type, std::move(sum));
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Take two JSON-encoded set values (e.g. {"SS": [...the actual list]}) and
|
||||
// return the difference of s1 - s2, again as a set value.
|
||||
// DynamoDB does not allow empty sets, so if resulting set is empty, return
|
||||
// an unset optional instead.
|
||||
std::optional<rjson::value> set_diff(const rjson::value& v1, const rjson::value& v2) {
|
||||
auto [set1_type, set1] = unwrap_set(v1);
|
||||
auto [set2_type, set2] = unwrap_set(v2);
|
||||
if (set1_type != set2_type) {
|
||||
throw api_error::validation(format("Mismatched set types: {} and {}", set1_type, set2_type));
|
||||
}
|
||||
if (!set1 || !set2) {
|
||||
throw api_error::validation("UpdateExpression: DELETE operation can only be performed on a set");
|
||||
}
|
||||
std::set<rjson::value, rjson::single_value_comp> set1_raw;
|
||||
for (auto it = set1->Begin(); it != set1->End(); ++it) {
|
||||
set1_raw.insert(rjson::copy(*it));
|
||||
}
|
||||
for (const auto& a : set2->GetArray()) {
|
||||
set1_raw.erase(a);
|
||||
}
|
||||
if (set1_raw.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
rjson::value ret = rjson::empty_object();
|
||||
rjson::set_with_string_name(ret, set1_type, rjson::empty_array());
|
||||
rjson::value& result_set = ret[set1_type];
|
||||
for (const auto& a : set1_raw) {
|
||||
rjson::push_back(result_set, rjson::copy(a));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include "types.hh"
|
||||
#include "schema_fwd.hh"
|
||||
#include "schema.hh"
|
||||
#include "keys.hh"
|
||||
#include "utils/rjson.hh"
|
||||
#include "rjson.hh"
|
||||
#include "utils/big_decimal.hh"
|
||||
|
||||
namespace alternator {
|
||||
@@ -45,7 +45,7 @@ struct type_representation {
|
||||
data_type dtype;
|
||||
};
|
||||
|
||||
type_info type_info_from_string(std::string_view type);
|
||||
type_info type_info_from_string(std::string type);
|
||||
type_representation represent_type(alternator_type atype);
|
||||
|
||||
bytes serialize_item(const rjson::value& item);
|
||||
@@ -54,7 +54,7 @@ rjson::value deserialize_item(bytes_view bv);
|
||||
std::string type_to_string(data_type type);
|
||||
|
||||
bytes get_key_column_value(const rjson::value& item, const column_definition& column);
|
||||
bytes get_key_from_typed_value(const rjson::value& key_typed_value, const column_definition& column);
|
||||
bytes get_key_from_typed_value(const rjson::value& key_typed_value, const column_definition& column, const std::string& expected_type);
|
||||
rjson::value json_key_column_value(bytes_view cell, const column_definition& column);
|
||||
|
||||
partition_key pk_from_json(const rjson::value& item, schema_ptr schema);
|
||||
@@ -69,21 +69,4 @@ big_decimal unwrap_number(const rjson::value& v, std::string_view diagnostic);
|
||||
// returned value is {"", nullptr}
|
||||
const std::pair<std::string, const rjson::value*> unwrap_set(const rjson::value& v);
|
||||
|
||||
// Check if a given JSON object encodes a list (i.e., it is a {"L": [...]}
|
||||
// and returns a pointer to that list.
|
||||
const rjson::value* unwrap_list(const rjson::value& v);
|
||||
|
||||
// Take two JSON-encoded numeric values ({"N": "thenumber"}) and return the
|
||||
// sum, again as a JSON-encoded number.
|
||||
rjson::value number_add(const rjson::value& v1, const rjson::value& v2);
|
||||
rjson::value number_subtract(const rjson::value& v1, const rjson::value& v2);
|
||||
// Take two JSON-encoded set values (e.g. {"SS": [...the actual set]}) and
|
||||
// return the sum of both sets, again as a set value.
|
||||
rjson::value set_sum(const rjson::value& v1, const rjson::value& v2);
|
||||
// Take two JSON-encoded set values (e.g. {"SS": [...the actual list]}) and
|
||||
// return the difference of s1 - s2, again as a set value.
|
||||
// DynamoDB does not allow empty sets, so if resulting set is empty, return
|
||||
// an unset optional instead.
|
||||
std::optional<rjson::value> set_diff(const rjson::value& v1, const rjson::value& v2);
|
||||
|
||||
}
|
||||
|
||||
@@ -23,14 +23,12 @@
|
||||
#include "log.hh"
|
||||
#include <seastar/http/function_handlers.hh>
|
||||
#include <seastar/json/json_elements.hh>
|
||||
#include "seastarx.hh"
|
||||
#include <seastarx.hh>
|
||||
#include "error.hh"
|
||||
#include "utils/rjson.hh"
|
||||
#include "rjson.hh"
|
||||
#include "auth.hh"
|
||||
#include <cctype>
|
||||
#include "cql3/query_processor.hh"
|
||||
#include "service/storage_service.hh"
|
||||
#include "utils/overloaded_functor.hh"
|
||||
|
||||
static logging::logger slogger("alternator-server");
|
||||
|
||||
@@ -67,42 +65,41 @@ inline std::vector<std::string_view> split(std::string_view text, char separator
|
||||
// Internal Server Error.
|
||||
class api_handler : public handler_base {
|
||||
public:
|
||||
api_handler(const std::function<future<executor::request_return_type>(std::unique_ptr<request> req)>& _handle) : _f_handle(
|
||||
[this, _handle](std::unique_ptr<request> req, std::unique_ptr<reply> rep) {
|
||||
return seastar::futurize_invoke(_handle, std::move(req)).then_wrapped([this, rep = std::move(rep)](future<executor::request_return_type> resf) mutable {
|
||||
api_handler(const future_json_function& _handle) : _f_handle(
|
||||
[_handle](std::unique_ptr<request> req, std::unique_ptr<reply> rep) {
|
||||
return seastar::futurize_apply(_handle, std::move(req)).then_wrapped([rep = std::move(rep)](future<json::json_return_type> resf) mutable {
|
||||
if (resf.failed()) {
|
||||
// Exceptions of type api_error are wrapped as JSON and
|
||||
// returned to the client as expected. Other types of
|
||||
// exceptions are unexpected, and returned to the user
|
||||
// as an internal server error:
|
||||
api_error ret;
|
||||
try {
|
||||
resf.get();
|
||||
} catch (api_error &ae) {
|
||||
generate_error_reply(*rep, ae);
|
||||
ret = ae;
|
||||
} catch (rjson::error & re) {
|
||||
generate_error_reply(*rep,
|
||||
api_error::validation(re.what()));
|
||||
ret = api_error("ValidationException", re.what());
|
||||
} catch (...) {
|
||||
generate_error_reply(*rep,
|
||||
api_error::internal(format("Internal server error: {}", std::current_exception())));
|
||||
ret = api_error(
|
||||
"Internal Server Error",
|
||||
format("Internal server error: {}", std::current_exception()),
|
||||
reply::status_type::internal_server_error);
|
||||
}
|
||||
// FIXME: what is this version number?
|
||||
rep->_content += "{\"__type\":\"com.amazonaws.dynamodb.v20120810#" + ret._type + "\"," +
|
||||
"\"message\":\"" + ret._msg + "\"}";
|
||||
rep->_status = ret._http_code;
|
||||
slogger.trace("api_handler error case: {}", rep->_content);
|
||||
return make_ready_future<std::unique_ptr<reply>>(std::move(rep));
|
||||
}
|
||||
slogger.trace("api_handler success case");
|
||||
auto res = resf.get0();
|
||||
std::visit(overloaded_functor {
|
||||
[&] (const json::json_return_type& json_return_value) {
|
||||
slogger.trace("api_handler success case");
|
||||
if (json_return_value._body_writer) {
|
||||
rep->write_body("json", std::move(json_return_value._body_writer));
|
||||
} else {
|
||||
rep->_content += json_return_value._res;
|
||||
}
|
||||
},
|
||||
[&] (const api_error& err) {
|
||||
generate_error_reply(*rep, err);
|
||||
}
|
||||
}, res);
|
||||
|
||||
if (res._body_writer) {
|
||||
rep->write_body("json", std::move(res._body_writer));
|
||||
} else {
|
||||
rep->_content += res._res;
|
||||
}
|
||||
return make_ready_future<std::unique_ptr<reply>>(std::move(rep));
|
||||
});
|
||||
}), _type("json") { }
|
||||
@@ -118,66 +115,18 @@ public:
|
||||
}
|
||||
|
||||
protected:
|
||||
void generate_error_reply(reply& rep, const api_error& err) {
|
||||
rep._content += "{\"__type\":\"com.amazonaws.dynamodb.v20120810#" + err._type + "\"," +
|
||||
"\"message\":\"" + err._msg + "\"}";
|
||||
rep._status = err._http_code;
|
||||
slogger.trace("api_handler error case: {}", rep._content);
|
||||
}
|
||||
|
||||
future_handler_function _f_handle;
|
||||
sstring _type;
|
||||
};
|
||||
|
||||
class gated_handler : public handler_base {
|
||||
seastar::gate& _gate;
|
||||
public:
|
||||
gated_handler(seastar::gate& gate) : _gate(gate) {}
|
||||
virtual future<std::unique_ptr<reply>> do_handle(const sstring& path, std::unique_ptr<request> req, std::unique_ptr<reply> rep) = 0;
|
||||
virtual future<std::unique_ptr<reply>> handle(const sstring& path, std::unique_ptr<request> req, std::unique_ptr<reply> rep) final override {
|
||||
return with_gate(_gate, [this, &path, req = std::move(req), rep = std::move(rep)] () mutable {
|
||||
return do_handle(path, std::move(req), std::move(rep));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
class health_handler : public gated_handler {
|
||||
public:
|
||||
health_handler(seastar::gate& pending_requests) : gated_handler(pending_requests) {}
|
||||
protected:
|
||||
virtual future<std::unique_ptr<reply>> do_handle(const sstring& path, std::unique_ptr<request> req, std::unique_ptr<reply> rep) override {
|
||||
class health_handler : public handler_base {
|
||||
virtual future<std::unique_ptr<reply>> handle(const sstring& path, std::unique_ptr<request> req, std::unique_ptr<reply> rep) override {
|
||||
rep->set_status(reply::status_type::ok);
|
||||
rep->write_body("txt", format("healthy: {}", req->get_header("Host")));
|
||||
return make_ready_future<std::unique_ptr<reply>>(std::move(rep));
|
||||
}
|
||||
};
|
||||
|
||||
class local_nodelist_handler : public gated_handler {
|
||||
public:
|
||||
local_nodelist_handler(seastar::gate& pending_requests) : gated_handler(pending_requests) {}
|
||||
protected:
|
||||
virtual future<std::unique_ptr<reply>> do_handle(const sstring& path, std::unique_ptr<request> req, std::unique_ptr<reply> rep) override {
|
||||
rjson::value results = rjson::empty_array();
|
||||
// It's very easy to get a list of all live nodes on the cluster,
|
||||
// using gms::get_local_gossiper().get_live_members(). But getting
|
||||
// just the list of live nodes in this DC needs more elaborate code:
|
||||
sstring local_dc = locator::i_endpoint_snitch::get_local_snitch_ptr()->get_datacenter(
|
||||
utils::fb_utilities::get_broadcast_address());
|
||||
std::unordered_set<gms::inet_address> local_dc_nodes =
|
||||
service::get_local_storage_service().get_token_metadata().
|
||||
get_topology().get_datacenter_endpoints().at(local_dc);
|
||||
for (auto& ip : local_dc_nodes) {
|
||||
if (gms::get_local_gossiper().is_alive(ip)) {
|
||||
rjson::push_back(results, rjson::from_string(ip.to_sstring()));
|
||||
}
|
||||
}
|
||||
rep->set_status(reply::status_type::ok);
|
||||
rep->set_content_type("json");
|
||||
rep->_content = rjson::print(results);
|
||||
return make_ready_future<std::unique_ptr<reply>>(std::move(rep));
|
||||
}
|
||||
};
|
||||
|
||||
future<> server::verify_signature(const request& req) {
|
||||
if (!_enforce_authorization) {
|
||||
slogger.debug("Skipping authorization");
|
||||
@@ -185,11 +134,11 @@ future<> server::verify_signature(const request& req) {
|
||||
}
|
||||
auto host_it = req._headers.find("Host");
|
||||
if (host_it == req._headers.end()) {
|
||||
throw api_error::invalid_signature("Host header is mandatory for signature verification");
|
||||
throw api_error("InvalidSignatureException", "Host header is mandatory for signature verification");
|
||||
}
|
||||
auto authorization_it = req._headers.find("Authorization");
|
||||
if (authorization_it == req._headers.end()) {
|
||||
throw api_error::invalid_signature("Authorization header is mandatory for signature verification");
|
||||
if (host_it == req._headers.end()) {
|
||||
throw api_error("InvalidSignatureException", "Authorization header is mandatory for signature verification");
|
||||
}
|
||||
std::string host = host_it->second;
|
||||
std::vector<std::string_view> credentials_raw = split(authorization_it->second, ' ');
|
||||
@@ -201,7 +150,7 @@ future<> server::verify_signature(const request& req) {
|
||||
std::vector<std::string_view> entry_split = split(entry, '=');
|
||||
if (entry_split.size() != 2) {
|
||||
if (entry != "AWS4-HMAC-SHA256") {
|
||||
throw api_error::invalid_signature(format("Only AWS4-HMAC-SHA256 algorithm is supported. Found: {}", entry));
|
||||
throw api_error("InvalidSignatureException", format("Only AWS4-HMAC-SHA256 algorithm is supported. Found: {}", entry));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -222,7 +171,7 @@ future<> server::verify_signature(const request& req) {
|
||||
}
|
||||
std::vector<std::string_view> credential_split = split(credential, '/');
|
||||
if (credential_split.size() != 5) {
|
||||
throw api_error::validation(format("Incorrect credential information format: {}", credential));
|
||||
throw api_error("ValidationException", format("Incorrect credential information format: {}", credential));
|
||||
}
|
||||
std::string user(credential_split[0]);
|
||||
std::string datestamp(credential_split[1]);
|
||||
@@ -243,8 +192,8 @@ future<> server::verify_signature(const request& req) {
|
||||
}
|
||||
}
|
||||
|
||||
auto cache_getter = [&qp = _qp] (std::string username) {
|
||||
return get_key_from_roles(qp, std::move(username));
|
||||
auto cache_getter = [] (std::string username) {
|
||||
return get_key_from_roles(cql3::get_query_processor().local(), std::move(username));
|
||||
};
|
||||
return _key_cache.get_ptr(user, cache_getter).then([this, &req,
|
||||
user = std::move(user),
|
||||
@@ -260,46 +209,32 @@ future<> server::verify_signature(const request& req) {
|
||||
|
||||
if (signature != std::string_view(user_signature)) {
|
||||
_key_cache.remove(user);
|
||||
throw api_error::unrecognized_client("The security token included in the request is invalid.");
|
||||
throw api_error("UnrecognizedClientException", "The security token included in the request is invalid.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
future<executor::request_return_type> server::handle_api_request(std::unique_ptr<request>&& req) {
|
||||
_executor._stats.total_operations++;
|
||||
future<json::json_return_type> server::handle_api_request(std::unique_ptr<request>&& req) {
|
||||
_executor.local()._stats.total_operations++;
|
||||
sstring target = req->get_header(TARGET);
|
||||
std::vector<std::string_view> split_target = split(target, '.');
|
||||
//NOTICE(sarna): Target consists of Dynamo API version followed by a dot '.' and operation type (e.g. CreateTable)
|
||||
std::string op = split_target.empty() ? std::string() : std::string(split_target.back());
|
||||
slogger.trace("Request: {} {} {}", op, req->content, req->_headers);
|
||||
slogger.trace("Request: {} {}", op, req->content);
|
||||
return verify_signature(*req).then([this, op, req = std::move(req)] () mutable {
|
||||
auto callback_it = _callbacks.find(op);
|
||||
if (callback_it == _callbacks.end()) {
|
||||
_executor._stats.unsupported_operations++;
|
||||
throw api_error::unknown_operation(format("Unsupported operation {}", op));
|
||||
_executor.local()._stats.unsupported_operations++;
|
||||
throw api_error("UnknownOperationException",
|
||||
format("Unsupported operation {}", op));
|
||||
}
|
||||
return with_gate(_pending_requests, [this, callback_it = std::move(callback_it), op = std::move(op), req = std::move(req)] () mutable {
|
||||
//FIXME: Client state can provide more context, e.g. client's endpoint address
|
||||
// We use unique_ptr because client_state cannot be moved or copied
|
||||
return do_with(std::make_unique<executor::client_state>(executor::client_state::internal_tag()),
|
||||
[this, callback_it = std::move(callback_it), op = std::move(op), req = std::move(req)] (std::unique_ptr<executor::client_state>& client_state) mutable {
|
||||
tracing::trace_state_ptr trace_state = executor::maybe_trace_query(*client_state, op, req->content);
|
||||
tracing::trace(trace_state, op);
|
||||
// JSON parsing can allocate up to roughly 2x the size of the raw document, + a couple of bytes for maintenance.
|
||||
// FIXME: by this time, the whole HTTP request was already read, so some memory is already occupied.
|
||||
// Once HTTP allows working on streams, we should grab the permit *before* reading the HTTP payload.
|
||||
size_t mem_estimate = req->content.size() * 3 + 8000;
|
||||
auto units_fut = get_units(*_memory_limiter, mem_estimate);
|
||||
if (_memory_limiter->waiters()) {
|
||||
++_executor._stats.requests_blocked_memory;
|
||||
}
|
||||
return units_fut.then([this, callback_it = std::move(callback_it), &client_state, trace_state, req = std::move(req)] (semaphore_units<> units) mutable {
|
||||
return _json_parser.parse(req->content).then([this, callback_it = std::move(callback_it), &client_state, trace_state,
|
||||
units = std::move(units), req = std::move(req)] (rjson::value json_request) mutable {
|
||||
return callback_it->second(_executor, *client_state, trace_state, make_service_permit(std::move(units)), std::move(json_request), std::move(req)).finally([trace_state] {});
|
||||
});
|
||||
});
|
||||
});
|
||||
//FIXME: Client state can provide more context, e.g. client's endpoint address
|
||||
// We use unique_ptr because client_state cannot be moved or copied
|
||||
return do_with(std::make_unique<executor::client_state>(executor::client_state::internal_tag()), [this, callback_it = std::move(callback_it), op = std::move(op), req = std::move(req)] (std::unique_ptr<executor::client_state>& client_state) mutable {
|
||||
client_state->set_raw_keyspace(executor::KEYSPACE_NAME);
|
||||
tracing::trace_state_ptr trace_state = executor::maybe_trace_query(*client_state, op, req->content);
|
||||
tracing::trace(trace_state, op);
|
||||
return callback_it->second(_executor.local(), *client_state, trace_state, std::move(req)).finally([trace_state] {});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -309,104 +244,35 @@ void server::set_routes(routes& r) {
|
||||
return handle_api_request(std::move(req));
|
||||
});
|
||||
|
||||
r.put(operation_type::POST, "/", req_handler);
|
||||
r.put(operation_type::GET, "/", new health_handler(_pending_requests));
|
||||
// The "/localnodes" request is a new Alternator feature, not supported by
|
||||
// DynamoDB and not required for DynamoDB compatibility. It allows a
|
||||
// client to enquire - using a trivial HTTP request without requiring
|
||||
// authentication - the list of all live nodes in the same data center of
|
||||
// the Alternator cluster. The client can use this list to balance its
|
||||
// request load to all the nodes in the same geographical region.
|
||||
// Note that this API exposes - openly without authentication - the
|
||||
// information on the cluster's members inside one data center. We do not
|
||||
// consider this to be a security risk, because an attacker can already
|
||||
// scan an entire subnet for nodes responding to the health request,
|
||||
// or even just scan for open ports.
|
||||
r.put(operation_type::GET, "/localnodes", new local_nodelist_handler(_pending_requests));
|
||||
r.add(operation_type::POST, url("/"), req_handler);
|
||||
r.add(operation_type::GET, url("/"), new health_handler);
|
||||
}
|
||||
|
||||
//FIXME: A way to immediately invalidate the cache should be considered,
|
||||
// e.g. when the system table which stores the keys is changed.
|
||||
// For now, this propagation may take up to 1 minute.
|
||||
server::server(executor& exec, cql3::query_processor& qp)
|
||||
: _http_server("http-alternator")
|
||||
, _https_server("https-alternator")
|
||||
, _executor(exec)
|
||||
, _qp(qp)
|
||||
, _key_cache(1024, 1min, slogger)
|
||||
, _enforce_authorization(false)
|
||||
, _enabled_servers{}
|
||||
, _pending_requests{}
|
||||
server::server(seastar::sharded<executor>& e)
|
||||
: _executor(e), _key_cache(1024, 1min, slogger), _enforce_authorization(false)
|
||||
, _callbacks{
|
||||
{"CreateTable", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.create_table(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"DescribeTable", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.describe_table(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"DeleteTable", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.delete_table(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"UpdateTable", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.update_table(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"PutItem", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.put_item(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"UpdateItem", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.update_item(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"GetItem", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.get_item(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"DeleteItem", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.delete_item(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"ListTables", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.list_tables(client_state, std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"Scan", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.scan(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"DescribeEndpoints", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.describe_endpoints(client_state, std::move(permit), std::move(json_request), req->get_header("Host"));
|
||||
}},
|
||||
{"BatchWriteItem", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.batch_write_item(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"BatchGetItem", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.batch_get_item(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"Query", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.query(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"TagResource", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.tag_resource(client_state, std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"UntagResource", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.untag_resource(client_state, std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"ListTagsOfResource", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.list_tags_of_resource(client_state, std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"ListStreams", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.list_streams(client_state, std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"DescribeStream", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.describe_stream(client_state, std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"GetShardIterator", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.get_shard_iterator(client_state, std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"GetRecords", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.get_records(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"CreateTable", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, std::unique_ptr<request> req) {
|
||||
return e.maybe_create_keyspace().then([&e, &client_state, req = std::move(req), trace_state = std::move(trace_state)] () mutable { return e.create_table(client_state, std::move(trace_state), req->content); }); }
|
||||
},
|
||||
{"DescribeTable", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, std::unique_ptr<request> req) { return e.describe_table(client_state, std::move(trace_state), req->content); }},
|
||||
{"DeleteTable", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, std::unique_ptr<request> req) { return e.delete_table(client_state, std::move(trace_state), req->content); }},
|
||||
{"PutItem", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, std::unique_ptr<request> req) { return e.put_item(client_state, std::move(trace_state), req->content); }},
|
||||
{"UpdateItem", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, std::unique_ptr<request> req) { return e.update_item(client_state, std::move(trace_state), req->content); }},
|
||||
{"GetItem", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, std::unique_ptr<request> req) { return e.get_item(client_state, std::move(trace_state), req->content); }},
|
||||
{"DeleteItem", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, std::unique_ptr<request> req) { return e.delete_item(client_state, std::move(trace_state), req->content); }},
|
||||
{"ListTables", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, std::unique_ptr<request> req) { return e.list_tables(client_state, req->content); }},
|
||||
{"Scan", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, std::unique_ptr<request> req) { return e.scan(client_state, std::move(trace_state), req->content); }},
|
||||
{"DescribeEndpoints", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, std::unique_ptr<request> req) { return e.describe_endpoints(client_state, req->content, req->get_header("Host")); }},
|
||||
{"BatchWriteItem", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, std::unique_ptr<request> req) { return e.batch_write_item(client_state, std::move(trace_state), req->content); }},
|
||||
{"BatchGetItem", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, std::unique_ptr<request> req) { return e.batch_get_item(client_state, std::move(trace_state), req->content); }},
|
||||
{"Query", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, std::unique_ptr<request> req) { return e.query(client_state, std::move(trace_state), req->content); }},
|
||||
} {
|
||||
}
|
||||
|
||||
future<> server::init(net::inet_address addr, std::optional<uint16_t> port, std::optional<uint16_t> https_port, std::optional<tls::credentials_builder> creds,
|
||||
bool enforce_authorization, semaphore* memory_limiter) {
|
||||
_memory_limiter = memory_limiter;
|
||||
future<> server::init(net::inet_address addr, std::optional<uint16_t> port, std::optional<uint16_t> https_port, std::optional<tls::credentials_builder> creds, bool enforce_authorization) {
|
||||
_enforce_authorization = enforce_authorization;
|
||||
if (!port && !https_port) {
|
||||
return make_exception_future<>(std::runtime_error("Either regular port or TLS port"
|
||||
@@ -414,26 +280,25 @@ future<> server::init(net::inet_address addr, std::optional<uint16_t> port, std:
|
||||
}
|
||||
return seastar::async([this, addr, port, https_port, creds] {
|
||||
try {
|
||||
_executor.start().get();
|
||||
_executor.invoke_on_all([] (executor& e) {
|
||||
return e.start();
|
||||
}).get();
|
||||
|
||||
if (port) {
|
||||
set_routes(_http_server._routes);
|
||||
_http_server.set_content_length_limit(server::content_length_limit);
|
||||
_http_server.listen(socket_address{addr, *port}).get();
|
||||
_enabled_servers.push_back(std::ref(_http_server));
|
||||
_control.start().get();
|
||||
_control.set_routes(std::bind(&server::set_routes, this, std::placeholders::_1)).get();
|
||||
_control.listen(socket_address{addr, *port}).get();
|
||||
slogger.info("Alternator HTTP server listening on {} port {}", addr, *port);
|
||||
}
|
||||
if (https_port) {
|
||||
set_routes(_https_server._routes);
|
||||
_https_server.set_content_length_limit(server::content_length_limit);
|
||||
_https_server.set_tls_credentials(creds->build_reloadable_server_credentials([](const std::unordered_set<sstring>& files, std::exception_ptr ep) {
|
||||
if (ep) {
|
||||
slogger.warn("Exception loading {}: {}", files, ep);
|
||||
} else {
|
||||
slogger.info("Reloaded {}", files);
|
||||
}
|
||||
}).get0());
|
||||
_https_server.listen(socket_address{addr, *https_port}).get();
|
||||
_enabled_servers.push_back(std::ref(_https_server));
|
||||
_https_control.start().get();
|
||||
_https_control.set_routes(std::bind(&server::set_routes, this, std::placeholders::_1)).get();
|
||||
_https_control.server().invoke_on_all([creds] (http_server& serv) {
|
||||
return serv.set_tls_credentials(creds->build_server_credentials());
|
||||
}).get();
|
||||
|
||||
_https_control.listen(socket_address{addr, *https_port}).get();
|
||||
slogger.info("Alternator HTTPS server listening on {} port {}", addr, *https_port);
|
||||
}
|
||||
} catch (...) {
|
||||
slogger.error("Failed to set up Alternator HTTP server on {} port {}, TLS port {}: {}",
|
||||
@@ -445,55 +310,5 @@ future<> server::init(net::inet_address addr, std::optional<uint16_t> port, std:
|
||||
});
|
||||
}
|
||||
|
||||
future<> server::stop() {
|
||||
return parallel_for_each(_enabled_servers, [] (http_server& server) {
|
||||
return server.stop();
|
||||
}).then([this] {
|
||||
return _pending_requests.close();
|
||||
}).then([this] {
|
||||
return _json_parser.stop();
|
||||
});
|
||||
}
|
||||
|
||||
server::json_parser::json_parser() : _run_parse_json_thread(async([this] {
|
||||
while (true) {
|
||||
_document_waiting.wait().get();
|
||||
if (_as.abort_requested()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
_parsed_document = rjson::parse_yieldable(_raw_document);
|
||||
_current_exception = nullptr;
|
||||
} catch (...) {
|
||||
_current_exception = std::current_exception();
|
||||
}
|
||||
_document_parsed.signal();
|
||||
}
|
||||
})) {
|
||||
}
|
||||
|
||||
future<rjson::value> server::json_parser::parse(std::string_view content) {
|
||||
if (content.size() < yieldable_parsing_threshold) {
|
||||
return make_ready_future<rjson::value>(rjson::parse(content));
|
||||
}
|
||||
return with_semaphore(_parsing_sem, 1, [this, content] {
|
||||
_raw_document = content;
|
||||
_document_waiting.signal();
|
||||
return _document_parsed.wait().then([this] {
|
||||
if (_current_exception) {
|
||||
return make_exception_future<rjson::value>(_current_exception);
|
||||
}
|
||||
return make_ready_future<rjson::value>(std::move(_parsed_document));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
future<> server::json_parser::stop() {
|
||||
_as.request_abort();
|
||||
_document_waiting.signal();
|
||||
_document_parsed.broken();
|
||||
return std::move(_run_parse_json_thread);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -26,58 +26,28 @@
|
||||
#include <seastar/http/httpd.hh>
|
||||
#include <seastar/net/tls.hh>
|
||||
#include <optional>
|
||||
#include "alternator/auth.hh"
|
||||
#include "utils/small_vector.hh"
|
||||
#include <seastar/core/units.hh>
|
||||
#include <alternator/auth.hh>
|
||||
|
||||
namespace alternator {
|
||||
|
||||
class server {
|
||||
static constexpr size_t content_length_limit = 16*MB;
|
||||
using alternator_callback = std::function<future<executor::request_return_type>(executor&, executor::client_state&,
|
||||
tracing::trace_state_ptr, service_permit, rjson::value, std::unique_ptr<request>)>;
|
||||
using alternator_callback = std::function<future<json::json_return_type>(executor&, executor::client_state&, tracing::trace_state_ptr, std::unique_ptr<request>)>;
|
||||
using alternator_callbacks_map = std::unordered_map<std::string_view, alternator_callback>;
|
||||
|
||||
http_server _http_server;
|
||||
http_server _https_server;
|
||||
executor& _executor;
|
||||
cql3::query_processor& _qp;
|
||||
|
||||
seastar::httpd::http_server_control _control;
|
||||
seastar::httpd::http_server_control _https_control;
|
||||
seastar::sharded<executor>& _executor;
|
||||
key_cache _key_cache;
|
||||
bool _enforce_authorization;
|
||||
utils::small_vector<std::reference_wrapper<seastar::httpd::http_server>, 2> _enabled_servers;
|
||||
gate _pending_requests;
|
||||
alternator_callbacks_map _callbacks;
|
||||
|
||||
semaphore* _memory_limiter;
|
||||
|
||||
class json_parser {
|
||||
static constexpr size_t yieldable_parsing_threshold = 16*KB;
|
||||
std::string_view _raw_document;
|
||||
rjson::value _parsed_document;
|
||||
std::exception_ptr _current_exception;
|
||||
semaphore _parsing_sem{1};
|
||||
condition_variable _document_waiting;
|
||||
condition_variable _document_parsed;
|
||||
abort_source _as;
|
||||
future<> _run_parse_json_thread;
|
||||
public:
|
||||
json_parser();
|
||||
future<rjson::value> parse(std::string_view content);
|
||||
future<> stop();
|
||||
};
|
||||
json_parser _json_parser;
|
||||
|
||||
public:
|
||||
server(executor& executor, cql3::query_processor& qp);
|
||||
server(seastar::sharded<executor>& executor);
|
||||
|
||||
future<> init(net::inet_address addr, std::optional<uint16_t> port, std::optional<uint16_t> https_port, std::optional<tls::credentials_builder> creds,
|
||||
bool enforce_authorization, semaphore* memory_limiter);
|
||||
future<> stop();
|
||||
seastar::future<> init(net::inet_address addr, std::optional<uint16_t> port, std::optional<uint16_t> https_port, std::optional<tls::credentials_builder> creds, bool enforce_authorization);
|
||||
private:
|
||||
void set_routes(seastar::httpd::routes& r);
|
||||
future<> verify_signature(const seastar::httpd::request& r);
|
||||
future<executor::request_return_type> handle_api_request(std::unique_ptr<request>&& req);
|
||||
future<json::json_return_type> handle_api_request(std::unique_ptr<request>&& req);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
#include "stats.hh"
|
||||
#include "utils/histogram_metrics_helper.hh"
|
||||
|
||||
#include <seastar/core/metrics.hh>
|
||||
|
||||
namespace alternator {
|
||||
@@ -37,7 +37,7 @@ stats::stats() : api_operations{} {
|
||||
seastar::metrics::description("number of operations via Alternator API"), {op(CamelCaseName)}),
|
||||
#define OPERATION_LATENCY(name, CamelCaseName) \
|
||||
seastar::metrics::make_histogram("op_latency", \
|
||||
seastar::metrics::description("Latency histogram of an operation via Alternator API"), {op(CamelCaseName)}, [this]{return to_metrics_histogram(api_operations.name);}),
|
||||
seastar::metrics::description("Latency histogram of an operation via Alternator API"), {op(CamelCaseName)}, [this]{return api_operations.name.get_histogram(1,20);}),
|
||||
OPERATION(batch_write_item, "BatchWriteItem")
|
||||
OPERATION(create_backup, "CreateBackup")
|
||||
OPERATION(create_global_table, "CreateGlobalTable")
|
||||
@@ -77,11 +77,6 @@ stats::stats() : api_operations{} {
|
||||
OPERATION_LATENCY(get_item_latency, "GetItem")
|
||||
OPERATION_LATENCY(delete_item_latency, "DeleteItem")
|
||||
OPERATION_LATENCY(update_item_latency, "UpdateItem")
|
||||
OPERATION(list_streams, "ListStreams")
|
||||
OPERATION(describe_stream, "DescribeStream")
|
||||
OPERATION(get_shard_iterator, "GetShardIterator")
|
||||
OPERATION(get_records, "GetRecords")
|
||||
OPERATION_LATENCY(get_records_latency, "GetRecords")
|
||||
});
|
||||
_metrics.add_group("alternator", {
|
||||
seastar::metrics::make_total_operations("unsupported_operations", unsupported_operations,
|
||||
@@ -90,12 +85,6 @@ stats::stats() : api_operations{} {
|
||||
seastar::metrics::description("number of total operations via Alternator API")),
|
||||
seastar::metrics::make_total_operations("reads_before_write", reads_before_write,
|
||||
seastar::metrics::description("number of performed read-before-write operations")),
|
||||
seastar::metrics::make_total_operations("write_using_lwt", write_using_lwt,
|
||||
seastar::metrics::description("number of writes that used LWT")),
|
||||
seastar::metrics::make_total_operations("shard_bounce_for_lwt", shard_bounce_for_lwt,
|
||||
seastar::metrics::description("number writes that had to be bounced from this shard because of LWT requirements")),
|
||||
seastar::metrics::make_total_operations("requests_blocked_memory", requests_blocked_memory,
|
||||
seastar::metrics::description("Counts a number of requests blocked due to memory pressure.")),
|
||||
seastar::metrics::make_total_operations("filtered_rows_read_total", cql_stats.filtered_rows_read_total,
|
||||
seastar::metrics::description("number of rows read during filtering operations")),
|
||||
seastar::metrics::make_total_operations("filtered_rows_matched_total", cql_stats.filtered_rows_matched_total,
|
||||
|
||||
@@ -74,24 +74,16 @@ public:
|
||||
uint64_t update_item = 0;
|
||||
uint64_t update_table = 0;
|
||||
uint64_t update_time_to_live = 0;
|
||||
uint64_t list_streams = 0;
|
||||
uint64_t describe_stream = 0;
|
||||
uint64_t get_shard_iterator = 0;
|
||||
uint64_t get_records = 0;
|
||||
|
||||
utils::time_estimated_histogram put_item_latency;
|
||||
utils::time_estimated_histogram get_item_latency;
|
||||
utils::time_estimated_histogram delete_item_latency;
|
||||
utils::time_estimated_histogram update_item_latency;
|
||||
utils::time_estimated_histogram get_records_latency;
|
||||
utils::estimated_histogram put_item_latency;
|
||||
utils::estimated_histogram get_item_latency;
|
||||
utils::estimated_histogram delete_item_latency;
|
||||
utils::estimated_histogram update_item_latency;
|
||||
} api_operations;
|
||||
// Miscellaneous event counters
|
||||
uint64_t total_operations = 0;
|
||||
uint64_t unsupported_operations = 0;
|
||||
uint64_t reads_before_write = 0;
|
||||
uint64_t write_using_lwt = 0;
|
||||
uint64_t shard_bounce_for_lwt = 0;
|
||||
uint64_t requests_blocked_memory = 0;
|
||||
// CQL-derived stats
|
||||
cql3::cql_stats cql_stats;
|
||||
private:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla 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 Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "serializer.hh"
|
||||
#include "schema.hh"
|
||||
#include "db/extensions.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
class tags_extension : public schema_extension {
|
||||
public:
|
||||
static constexpr auto NAME = "scylla_tags";
|
||||
|
||||
tags_extension() = default;
|
||||
explicit tags_extension(const std::map<sstring, sstring>& tags) : _tags(std::move(tags)) {}
|
||||
explicit tags_extension(bytes b) : _tags(tags_extension::deserialize(b)) {}
|
||||
explicit tags_extension(const sstring& s) {
|
||||
throw std::logic_error("Cannot create tags from string");
|
||||
}
|
||||
bytes serialize() const override {
|
||||
return ser::serialize_to_buffer<bytes>(_tags);
|
||||
}
|
||||
static std::map<sstring, sstring> deserialize(bytes_view buffer) {
|
||||
return ser::deserialize_from_buffer(buffer, boost::type<std::map<sstring, sstring>>());
|
||||
}
|
||||
const std::map<sstring, sstring>& tags() const {
|
||||
return _tags;
|
||||
}
|
||||
private:
|
||||
std::map<sstring, sstring> _tags;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -70,7 +70,7 @@
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Force a major compaction of this column family",
|
||||
"type":"void",
|
||||
"type":"string",
|
||||
"nickname":"force_major_compaction",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -380,54 +380,16 @@
|
||||
"operations":[
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"check if the auto_compaction property is enabled for a given table",
|
||||
"summary":"check if the auto compaction disabled",
|
||||
"type":"boolean",
|
||||
"nickname":"get_auto_compaction",
|
||||
"nickname":"is_auto_compaction_disabled",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"name",
|
||||
"description":"The table name in keyspace:name format",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"path"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Enable table auto compaction",
|
||||
"type":"void",
|
||||
"nickname":"enable_auto_compaction",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"name",
|
||||
"description":"The table name in keyspace:name format",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"path"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method":"DELETE",
|
||||
"summary":"Disable table auto compaction",
|
||||
"type":"void",
|
||||
"nickname":"disable_auto_compaction",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"name",
|
||||
"description":"The table name in keyspace:name format",
|
||||
"description":"The column family name in keyspace:name format",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
{
|
||||
"apiVersion":"0.0.1",
|
||||
"swaggerVersion":"1.2",
|
||||
"basePath":"{{Protocol}}://{{Host}}",
|
||||
"resourcePath":"/error_injection",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"apis":[
|
||||
{
|
||||
"path":"/v2/error_injection/injection/{injection}",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Activate an injection that triggers an error in code",
|
||||
"type":"void",
|
||||
"nickname":"enable_injection",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"injection",
|
||||
"description":"injection name, should correspond to an injection added in code",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"path"
|
||||
},
|
||||
{
|
||||
"name":"one_shot",
|
||||
"description":"boolean flag indicating whether the injection should be enabled to trigger only once",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"boolean",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method":"DELETE",
|
||||
"summary":"Deactivate an injection previously activated by the API",
|
||||
"type":"void",
|
||||
"nickname":"disable_injection",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"injection",
|
||||
"description":"injection name",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/v2/error_injection/injection",
|
||||
"operations":[
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"List all enabled injections on all shards, i.e. injections that will trigger an error in the code",
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type":"string"
|
||||
},
|
||||
"nickname":"get_enabled_injections_on_all",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[]
|
||||
},
|
||||
{
|
||||
"method":"DELETE",
|
||||
"summary":"Deactivate all injections previously activated on all shards by the API",
|
||||
"type":"void",
|
||||
"nickname":"disable_on_all",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -249,7 +249,7 @@
|
||||
"MIGRATION_REQUEST",
|
||||
"PREPARE_MESSAGE",
|
||||
"PREPARE_DONE_MESSAGE",
|
||||
"UNUSED__STREAM_MUTATION",
|
||||
"STREAM_MUTATION",
|
||||
"STREAM_MUTATION_DONE",
|
||||
"COMPLETE_MESSAGE",
|
||||
"REPAIR_CHECKSUM_RANGE",
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"summary":"Get the hinted handoff enabled by dc",
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type":"array"
|
||||
"type":"mapper_list"
|
||||
},
|
||||
"nickname":"get_hinted_handoff_enabled_by_dc",
|
||||
"produces":[
|
||||
@@ -641,21 +641,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/storage_proxy/metrics/cas_write/failed_read_round_optimization",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get cas write metrics",
|
||||
"type": "long",
|
||||
"nickname": "get_cas_write_metrics_failed_read_round_optimization",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/storage_proxy/metrics/cas_read/unfinished_commit",
|
||||
"operations": [
|
||||
|
||||
@@ -511,21 +511,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/cdc_streams_check_and_repair",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Checks that CDC streams reflect current cluster topology and regenerates them if not.",
|
||||
"type":"void",
|
||||
"nickname":"cdc_streams_check_and_repair",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/snapshots",
|
||||
"operations":[
|
||||
@@ -597,15 +582,7 @@
|
||||
},
|
||||
{
|
||||
"name":"kn",
|
||||
"description":"Comma seperated keyspaces name that their snapshot will be deleted",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"cf",
|
||||
"description":"an optional table name that its snapshot will be deleted",
|
||||
"description":"Comma seperated keyspaces name to snapshot",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
@@ -833,43 +810,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/repair_status/",
|
||||
"operations":[
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Query the repair status and return when the repair is finished or timeout",
|
||||
"type":"string",
|
||||
"enum":[
|
||||
"RUNNING",
|
||||
"SUCCESSFUL",
|
||||
"FAILED"
|
||||
],
|
||||
"nickname":"repair_await_completion",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"id",
|
||||
"description":"The repair ID to check for status",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"timeout",
|
||||
"description":"Seconds to wait before the query returns even if the repair is not finished. The value -1 or not providing this parameter means no timeout",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/repair_async/{keyspace}",
|
||||
"operations":[
|
||||
@@ -2468,7 +2408,7 @@
|
||||
"version":{
|
||||
"type":"string",
|
||||
"enum":[
|
||||
"ka", "la", "mc", "md"
|
||||
"ka", "la", "mc"
|
||||
],
|
||||
"description":"SSTable version"
|
||||
},
|
||||
|
||||
53
api/api.cc
53
api/api.cc
@@ -36,7 +36,6 @@
|
||||
#include "endpoint_snitch.hh"
|
||||
#include "compaction_manager.hh"
|
||||
#include "hinted_handoff.hh"
|
||||
#include "error_injection.hh"
|
||||
#include <seastar/http/exception.hh>
|
||||
#include "stream_manager.hh"
|
||||
#include "system.hh"
|
||||
@@ -69,19 +68,13 @@ future<> set_server_init(http_context& ctx) {
|
||||
rb->set_api_doc(r);
|
||||
rb02->set_api_doc(r);
|
||||
rb02->register_api_file(r, "swagger20_header");
|
||||
set_config(rb02, ctx, r);
|
||||
rb->register_function(r, "system",
|
||||
"The system related API");
|
||||
set_system(ctx, r);
|
||||
});
|
||||
}
|
||||
|
||||
future<> set_server_config(http_context& ctx) {
|
||||
auto rb02 = std::make_shared < api_registry_builder20 > (ctx.api_doc, "/v2");
|
||||
return ctx.http_server.set_routes([&ctx, rb02](routes& r) {
|
||||
set_config(rb02, ctx, r);
|
||||
});
|
||||
}
|
||||
|
||||
static future<> register_api(http_context& ctx, const sstring& api_name,
|
||||
const sstring api_desc,
|
||||
std::function<void(http_context& ctx, routes& r)> f) {
|
||||
@@ -93,42 +86,10 @@ static future<> register_api(http_context& ctx, const sstring& api_name,
|
||||
});
|
||||
}
|
||||
|
||||
future<> set_transport_controller(http_context& ctx, cql_transport::controller& ctl) {
|
||||
return ctx.http_server.set_routes([&ctx, &ctl] (routes& r) { set_transport_controller(ctx, r, ctl); });
|
||||
}
|
||||
|
||||
future<> unset_transport_controller(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_transport_controller(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_rpc_controller(http_context& ctx, thrift_controller& ctl) {
|
||||
return ctx.http_server.set_routes([&ctx, &ctl] (routes& r) { set_rpc_controller(ctx, r, ctl); });
|
||||
}
|
||||
|
||||
future<> unset_rpc_controller(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_rpc_controller(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_server_storage_service(http_context& ctx) {
|
||||
return register_api(ctx, "storage_service", "The storage service API", set_storage_service);
|
||||
}
|
||||
|
||||
future<> set_server_repair(http_context& ctx, sharded<netw::messaging_service>& ms) {
|
||||
return ctx.http_server.set_routes([&ctx, &ms] (routes& r) { set_repair(ctx, r, ms); });
|
||||
}
|
||||
|
||||
future<> unset_server_repair(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_repair(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_server_snapshot(http_context& ctx, sharded<db::snapshot_ctl>& snap_ctl) {
|
||||
return ctx.http_server.set_routes([&ctx, &snap_ctl] (routes& r) { set_snapshot(ctx, r, snap_ctl); });
|
||||
}
|
||||
|
||||
future<> unset_server_snapshot(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_snapshot(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_server_snitch(http_context& ctx) {
|
||||
return register_api(ctx, "endpoint_snitch_info", "The endpoint snitch info API", set_endpoint_snitch);
|
||||
}
|
||||
@@ -143,14 +104,9 @@ future<> set_server_load_sstable(http_context& ctx) {
|
||||
"The column family API", set_column_family);
|
||||
}
|
||||
|
||||
future<> set_server_messaging_service(http_context& ctx, sharded<netw::messaging_service>& ms) {
|
||||
future<> set_server_messaging_service(http_context& ctx) {
|
||||
return register_api(ctx, "messaging_service",
|
||||
"The messaging service API", [&ms] (http_context& ctx, routes& r) {
|
||||
set_messaging_service(ctx, r, ms);
|
||||
});
|
||||
}
|
||||
future<> unset_server_messaging_service(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_messaging_service(ctx, r); });
|
||||
"The messaging service API", set_messaging_service);
|
||||
}
|
||||
|
||||
future<> set_server_storage_proxy(http_context& ctx) {
|
||||
@@ -197,9 +153,6 @@ future<> set_server_done(http_context& ctx) {
|
||||
rb->register_function(r, "collectd",
|
||||
"The collectd API");
|
||||
set_collectd(ctx, r);
|
||||
rb->register_function(r, "error_injection",
|
||||
"The error injection API");
|
||||
set_error_injection(ctx, r);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -256,6 +256,4 @@ public:
|
||||
operator T() const { return value; }
|
||||
};
|
||||
|
||||
utils_json::estimated_histogram time_to_json_histogram(const utils::time_estimated_histogram& val);
|
||||
|
||||
}
|
||||
|
||||
@@ -24,11 +24,6 @@
|
||||
#include <seastar/http/httpd.hh>
|
||||
|
||||
namespace service { class load_meter; }
|
||||
namespace locator { class shared_token_metadata; }
|
||||
namespace cql_transport { class controller; }
|
||||
class thrift_controller;
|
||||
namespace db { class snapshot_ctl; }
|
||||
namespace netw { class messaging_service; }
|
||||
|
||||
namespace api {
|
||||
|
||||
@@ -39,33 +34,19 @@ struct http_context {
|
||||
distributed<database>& db;
|
||||
distributed<service::storage_proxy>& sp;
|
||||
service::load_meter& lmeter;
|
||||
const sharded<locator::shared_token_metadata>& shared_token_metadata;
|
||||
|
||||
http_context(distributed<database>& _db,
|
||||
distributed<service::storage_proxy>& _sp,
|
||||
service::load_meter& _lm, const sharded<locator::shared_token_metadata>& _stm)
|
||||
: db(_db), sp(_sp), lmeter(_lm), shared_token_metadata(_stm) {
|
||||
service::load_meter& _lm)
|
||||
: db(_db), sp(_sp), lmeter(_lm) {
|
||||
}
|
||||
|
||||
const locator::token_metadata& get_token_metadata();
|
||||
};
|
||||
|
||||
future<> set_server_init(http_context& ctx);
|
||||
future<> set_server_config(http_context& ctx);
|
||||
future<> set_server_snitch(http_context& ctx);
|
||||
future<> set_server_storage_service(http_context& ctx);
|
||||
future<> set_server_repair(http_context& ctx, sharded<netw::messaging_service>& ms);
|
||||
future<> unset_server_repair(http_context& ctx);
|
||||
future<> set_transport_controller(http_context& ctx, cql_transport::controller& ctl);
|
||||
future<> unset_transport_controller(http_context& ctx);
|
||||
future<> set_rpc_controller(http_context& ctx, thrift_controller& ctl);
|
||||
future<> unset_rpc_controller(http_context& ctx);
|
||||
future<> set_server_snapshot(http_context& ctx, sharded<db::snapshot_ctl>& snap_ctl);
|
||||
future<> unset_server_snapshot(http_context& ctx);
|
||||
future<> set_server_gossip(http_context& ctx);
|
||||
future<> set_server_load_sstable(http_context& ctx);
|
||||
future<> set_server_messaging_service(http_context& ctx, sharded<netw::messaging_service>& ms);
|
||||
future<> unset_server_messaging_service(http_context& ctx);
|
||||
future<> set_server_messaging_service(http_context& ctx);
|
||||
future<> set_server_storage_proxy(http_context& ctx);
|
||||
future<> set_server_stream_manager(http_context& ctx);
|
||||
future<> set_server_gossip_settle(http_context& ctx);
|
||||
|
||||
@@ -208,11 +208,9 @@ void set_cache_service(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
cs::get_row_capacity.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return ctx.db.map_reduce0([](database& db) -> uint64_t {
|
||||
return db.row_cache_tracker().region().occupancy().used_space();
|
||||
}, uint64_t(0), std::plus<uint64_t>()).then([](const int64_t& res) {
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
return map_reduce_cf(ctx, uint64_t(0), [](const column_family& cf) {
|
||||
return cf.get_row_cache().get_cache_tracker().region().occupancy().used_space();
|
||||
}, std::plus<uint64_t>());
|
||||
});
|
||||
|
||||
cs::get_row_hits.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
@@ -253,19 +251,15 @@ void set_cache_service(http_context& ctx, routes& r) {
|
||||
cs::get_row_size.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
// In origin row size is the weighted size.
|
||||
// We currently do not support weights, so we use num entries instead
|
||||
return ctx.db.map_reduce0([](database& db) -> uint64_t {
|
||||
return db.row_cache_tracker().partitions();
|
||||
}, uint64_t(0), std::plus<uint64_t>()).then([](const int64_t& res) {
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
return map_reduce_cf(ctx, 0, [](const column_family& cf) {
|
||||
return cf.get_row_cache().partitions();
|
||||
}, std::plus<uint64_t>());
|
||||
});
|
||||
|
||||
cs::get_row_entries.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return ctx.db.map_reduce0([](database& db) -> uint64_t {
|
||||
return db.row_cache_tracker().partitions();
|
||||
}, uint64_t(0), std::plus<uint64_t>()).then([](const int64_t& res) {
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
return map_reduce_cf(ctx, 0, [](const column_family& cf) {
|
||||
return cf.get_row_cache().partitions();
|
||||
}, std::plus<uint64_t>());
|
||||
});
|
||||
|
||||
cs::get_counter_capacity.set(r, [] (std::unique_ptr<request> req) {
|
||||
|
||||
@@ -64,7 +64,7 @@ static const char* str_to_regex(const sstring& v) {
|
||||
void set_collectd(http_context& ctx, routes& r) {
|
||||
cd::get_collectd.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
|
||||
auto id = ::make_shared<scollectd::type_instance_id>(req->param["pluginid"],
|
||||
auto id = make_shared<scollectd::type_instance_id>(req->param["pluginid"],
|
||||
req->get_query_param("instance"), req->get_query_param("type"),
|
||||
req->get_query_param("type_instance"));
|
||||
|
||||
|
||||
@@ -249,12 +249,6 @@ static future<json::json_return_type> sum_sstable(http_context& ctx, bool total)
|
||||
});
|
||||
}
|
||||
|
||||
future<json::json_return_type> map_reduce_cf_time_histogram(http_context& ctx, const sstring& name, std::function<utils::time_estimated_histogram(const column_family&)> f) {
|
||||
return map_reduce_cf_raw(ctx, name, utils::time_estimated_histogram(), f, utils::time_estimated_histogram_merge).then([](const utils::time_estimated_histogram& res) {
|
||||
return make_ready_future<json::json_return_type>(time_to_json_histogram(res));
|
||||
});
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
class sum_ratio {
|
||||
uint64_t _n = 0;
|
||||
@@ -802,21 +796,24 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
cf::get_cas_prepare.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf_time_histogram(ctx, req->param["name"], [](const column_family& cf) {
|
||||
return map_reduce_cf(ctx, req->param["name"], utils::estimated_histogram(0), [](column_family& cf) {
|
||||
return cf.get_stats().estimated_cas_prepare;
|
||||
});
|
||||
},
|
||||
utils::estimated_histogram_merge, utils_json::estimated_histogram());
|
||||
});
|
||||
|
||||
cf::get_cas_propose.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf_time_histogram(ctx, req->param["name"], [](const column_family& cf) {
|
||||
return cf.get_stats().estimated_cas_accept;
|
||||
});
|
||||
return map_reduce_cf(ctx, req->param["name"], utils::estimated_histogram(0), [](column_family& cf) {
|
||||
return cf.get_stats().estimated_cas_propose;
|
||||
},
|
||||
utils::estimated_histogram_merge, utils_json::estimated_histogram());
|
||||
});
|
||||
|
||||
cf::get_cas_commit.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf_time_histogram(ctx, req->param["name"], [](const column_family& cf) {
|
||||
return cf.get_stats().estimated_cas_learn;
|
||||
});
|
||||
return map_reduce_cf(ctx, req->param["name"], utils::estimated_histogram(0), [](column_family& cf) {
|
||||
return cf.get_stats().estimated_cas_commit;
|
||||
},
|
||||
utils::estimated_histogram_merge, utils_json::estimated_histogram());
|
||||
});
|
||||
|
||||
cf::get_sstables_per_read_histogram.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
@@ -842,32 +839,15 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
|
||||
cf::get_auto_compaction.set(r, [&ctx] (const_req req) {
|
||||
const utils::UUID& uuid = get_uuid(req.param["name"], ctx.db.local());
|
||||
column_family& cf = ctx.db.local().find_column_family(uuid);
|
||||
return !cf.is_auto_compaction_disabled_by_user();
|
||||
});
|
||||
|
||||
cf::enable_auto_compaction.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return foreach_column_family(ctx, req->param["name"], [](column_family &cf) {
|
||||
cf.enable_auto_compaction();
|
||||
}).then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
cf::disable_auto_compaction.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return foreach_column_family(ctx, req->param["name"], [](column_family &cf) {
|
||||
cf.disable_auto_compaction();
|
||||
}).then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
cf::is_auto_compaction_disabled.set(r, [] (const_req req) {
|
||||
// FIXME
|
||||
// currently auto compaction is disable
|
||||
// it should be changed when it would have an API
|
||||
return true;
|
||||
});
|
||||
|
||||
cf::get_built_indexes.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
auto ks_cf = parse_fully_qualified_cf_name(req->param["name"]);
|
||||
auto&& ks = std::get<0>(ks_cf);
|
||||
auto&& cf_name = std::get<1>(ks_cf);
|
||||
auto [ks, cf_name] = parse_fully_qualified_cf_name(req->param["name"]);
|
||||
return db::system_keyspace::load_view_build_progress().then([ks, cf_name, &ctx](const std::vector<db::system_keyspace::view_build_progress>& vb) mutable {
|
||||
std::set<sstring> vp;
|
||||
for (auto b : vb) {
|
||||
@@ -880,7 +860,7 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
column_family& cf = ctx.db.local().find_column_family(uuid);
|
||||
res.reserve(cf.get_index_manager().list_indexes().size());
|
||||
for (auto&& i : cf.get_index_manager().list_indexes()) {
|
||||
if (!vp.contains(secondary_index::index_table_name(i.metadata().name()))) {
|
||||
if (vp.find(secondary_index::index_table_name(i.metadata().name())) == vp.end()) {
|
||||
res.emplace_back(i.metadata().name());
|
||||
}
|
||||
}
|
||||
@@ -914,15 +894,17 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
cf::get_read_latency_estimated_histogram.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return map_reduce_cf_time_histogram(ctx, req->param["name"], [](const column_family& cf) {
|
||||
return map_reduce_cf(ctx, req->param["name"], utils::estimated_histogram(0), [](column_family& cf) {
|
||||
return cf.get_stats().estimated_read;
|
||||
});
|
||||
},
|
||||
utils::estimated_histogram_merge, utils_json::estimated_histogram());
|
||||
});
|
||||
|
||||
cf::get_write_latency_estimated_histogram.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return map_reduce_cf_time_histogram(ctx, req->param["name"], [](const column_family& cf) {
|
||||
return map_reduce_cf(ctx, req->param["name"], utils::estimated_histogram(0), [](column_family& cf) {
|
||||
return cf.get_stats().estimated_write;
|
||||
});
|
||||
},
|
||||
utils::estimated_histogram_merge, utils_json::estimated_histogram());
|
||||
});
|
||||
|
||||
cf::set_compaction_strategy_class.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
@@ -1012,15 +994,5 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
});
|
||||
});
|
||||
|
||||
cf::force_major_compaction.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
if (req->get_query_param("split_output") != "") {
|
||||
fail(unimplemented::cause::API);
|
||||
}
|
||||
return foreach_column_family(ctx, req->param["name"], [](column_family &cf) {
|
||||
return cf.compact_all_sstables();
|
||||
}).then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +68,6 @@ future<json::json_return_type> map_reduce_cf(http_context& ctx, const sstring& n
|
||||
});
|
||||
}
|
||||
|
||||
future<json::json_return_type> map_reduce_cf_time_histogram(http_context& ctx, const sstring& name, std::function<utils::time_estimated_histogram(const column_family&)> f);
|
||||
|
||||
struct map_reduce_column_families_locally {
|
||||
std::any init;
|
||||
std::function<std::unique_ptr<std::any>(column_family&)> mapper;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
#include "commitlog.hh"
|
||||
#include "db/commitlog/commitlog.hh"
|
||||
#include <db/commitlog/commitlog.hh>
|
||||
#include "api/api-doc/commitlog.json.hh"
|
||||
#include "database.hh"
|
||||
#include <vector>
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla 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 Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "api/api-doc/error_injection.json.hh"
|
||||
#include "api/api.hh"
|
||||
|
||||
#include <seastar/http/exception.hh>
|
||||
#include "log.hh"
|
||||
#include "utils/error_injection.hh"
|
||||
#include "seastar/core/future-util.hh"
|
||||
|
||||
namespace api {
|
||||
|
||||
namespace hf = httpd::error_injection_json;
|
||||
|
||||
void set_error_injection(http_context& ctx, routes& r) {
|
||||
|
||||
hf::enable_injection.set(r, [](std::unique_ptr<request> req) {
|
||||
sstring injection = req->param["injection"];
|
||||
bool one_shot = req->get_query_param("one_shot") == "True";
|
||||
auto& errinj = utils::get_local_injector();
|
||||
return errinj.enable_on_all(injection, one_shot).then([] {
|
||||
return make_ready_future<json::json_return_type>(json::json_void());
|
||||
});
|
||||
});
|
||||
|
||||
hf::get_enabled_injections_on_all.set(r, [](std::unique_ptr<request> req) {
|
||||
auto& errinj = utils::get_local_injector();
|
||||
auto ret = errinj.enabled_injections_on_all();
|
||||
return make_ready_future<json::json_return_type>(ret);
|
||||
});
|
||||
|
||||
hf::disable_injection.set(r, [](std::unique_ptr<request> req) {
|
||||
sstring injection = req->param["injection"];
|
||||
|
||||
auto& errinj = utils::get_local_injector();
|
||||
return errinj.disable_on_all(injection).then([] {
|
||||
return make_ready_future<json::json_return_type>(json::json_void());
|
||||
});
|
||||
});
|
||||
|
||||
hf::disable_on_all.set(r, [](std::unique_ptr<request> req) {
|
||||
auto& errinj = utils::get_local_injector();
|
||||
return errinj.disable_on_all().then([] {
|
||||
return make_ready_future<json::json_return_type>(json::json_void());
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
} // namespace api
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla 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 Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "api.hh"
|
||||
|
||||
namespace api {
|
||||
|
||||
void set_error_injection(http_context& ctx, routes& r);
|
||||
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
#include "gossiper.hh"
|
||||
#include "api/api-doc/gossiper.json.hh"
|
||||
#include "gms/gossiper.hh"
|
||||
#include <gms/gossiper.hh>
|
||||
|
||||
namespace api {
|
||||
using namespace json;
|
||||
|
||||
@@ -53,8 +53,8 @@ std::vector<message_counter> map_to_message_counters(
|
||||
* according to a function that it gets as a parameter.
|
||||
*
|
||||
*/
|
||||
future_json_function get_client_getter(sharded<netw::messaging_service>& ms, std::function<uint64_t(const shard_info&)> f) {
|
||||
return [&ms, f](std::unique_ptr<request> req) {
|
||||
future_json_function get_client_getter(std::function<uint64_t(const shard_info&)> f) {
|
||||
return [f](std::unique_ptr<request> req) {
|
||||
using map_type = std::unordered_map<gms::inet_address, uint64_t>;
|
||||
auto get_shard_map = [f](messaging_service& ms) {
|
||||
std::unordered_map<gms::inet_address, unsigned long> map;
|
||||
@@ -63,15 +63,15 @@ future_json_function get_client_getter(sharded<netw::messaging_service>& ms, std
|
||||
});
|
||||
return map;
|
||||
};
|
||||
return ms.map_reduce0(get_shard_map, map_type(), map_sum<map_type>).
|
||||
return get_messaging_service().map_reduce0(get_shard_map, map_type(), map_sum<map_type>).
|
||||
then([](map_type&& map) {
|
||||
return make_ready_future<json::json_return_type>(map_to_message_counters(map));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
future_json_function get_server_getter(sharded<netw::messaging_service>& ms, std::function<uint64_t(const rpc::stats&)> f) {
|
||||
return [&ms, f](std::unique_ptr<request> req) {
|
||||
future_json_function get_server_getter(std::function<uint64_t(const rpc::stats&)> f) {
|
||||
return [f](std::unique_ptr<request> req) {
|
||||
using map_type = std::unordered_map<gms::inet_address, uint64_t>;
|
||||
auto get_shard_map = [f](messaging_service& ms) {
|
||||
std::unordered_map<gms::inet_address, unsigned long> map;
|
||||
@@ -80,53 +80,53 @@ future_json_function get_server_getter(sharded<netw::messaging_service>& ms, std
|
||||
});
|
||||
return map;
|
||||
};
|
||||
return ms.map_reduce0(get_shard_map, map_type(), map_sum<map_type>).
|
||||
return get_messaging_service().map_reduce0(get_shard_map, map_type(), map_sum<map_type>).
|
||||
then([](map_type&& map) {
|
||||
return make_ready_future<json::json_return_type>(map_to_message_counters(map));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
void set_messaging_service(http_context& ctx, routes& r, sharded<netw::messaging_service>& ms) {
|
||||
get_timeout_messages.set(r, get_client_getter(ms, [](const shard_info& c) {
|
||||
void set_messaging_service(http_context& ctx, routes& r) {
|
||||
get_timeout_messages.set(r, get_client_getter([](const shard_info& c) {
|
||||
return c.get_stats().timeout;
|
||||
}));
|
||||
|
||||
get_sent_messages.set(r, get_client_getter(ms, [](const shard_info& c) {
|
||||
get_sent_messages.set(r, get_client_getter([](const shard_info& c) {
|
||||
return c.get_stats().sent_messages;
|
||||
}));
|
||||
|
||||
get_dropped_messages.set(r, get_client_getter(ms, [](const shard_info& c) {
|
||||
get_dropped_messages.set(r, get_client_getter([](const shard_info& c) {
|
||||
// We don't have the same drop message mechanism
|
||||
// as origin has.
|
||||
// hence we can always return 0
|
||||
return 0;
|
||||
}));
|
||||
|
||||
get_exception_messages.set(r, get_client_getter(ms, [](const shard_info& c) {
|
||||
get_exception_messages.set(r, get_client_getter([](const shard_info& c) {
|
||||
return c.get_stats().exception_received;
|
||||
}));
|
||||
|
||||
get_pending_messages.set(r, get_client_getter(ms, [](const shard_info& c) {
|
||||
get_pending_messages.set(r, get_client_getter([](const shard_info& c) {
|
||||
return c.get_stats().pending;
|
||||
}));
|
||||
|
||||
get_respond_pending_messages.set(r, get_server_getter(ms, [](const rpc::stats& c) {
|
||||
get_respond_pending_messages.set(r, get_server_getter([](const rpc::stats& c) {
|
||||
return c.pending;
|
||||
}));
|
||||
|
||||
get_respond_completed_messages.set(r, get_server_getter(ms, [](const rpc::stats& c) {
|
||||
get_respond_completed_messages.set(r, get_server_getter([](const rpc::stats& c) {
|
||||
return c.sent_messages;
|
||||
}));
|
||||
|
||||
get_version.set(r, [&ms](const_req req) {
|
||||
return ms.local().get_raw_version(req.get_query_param("addr"));
|
||||
get_version.set(r, [](const_req req) {
|
||||
return netw::get_local_messaging_service().get_raw_version(req.get_query_param("addr"));
|
||||
});
|
||||
|
||||
get_dropped_messages_by_ver.set(r, [&ms](std::unique_ptr<request> req) {
|
||||
get_dropped_messages_by_ver.set(r, [](std::unique_ptr<request> req) {
|
||||
shared_ptr<std::vector<uint64_t>> map = make_shared<std::vector<uint64_t>>(num_verb);
|
||||
|
||||
return ms.map_reduce([map](const uint64_t* local_map) mutable {
|
||||
return netw::get_messaging_service().map_reduce([map](const uint64_t* local_map) mutable {
|
||||
for (auto i = 0; i < num_verb; i++) {
|
||||
(*map)[i]+= local_map[i];
|
||||
}
|
||||
@@ -151,18 +151,5 @@ void set_messaging_service(http_context& ctx, routes& r, sharded<netw::messaging
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void unset_messaging_service(http_context& ctx, routes& r) {
|
||||
get_timeout_messages.unset(r);
|
||||
get_sent_messages.unset(r);
|
||||
get_dropped_messages.unset(r);
|
||||
get_exception_messages.unset(r);
|
||||
get_pending_messages.unset(r);
|
||||
get_respond_pending_messages.unset(r);
|
||||
get_respond_completed_messages.unset(r);
|
||||
get_version.unset(r);
|
||||
get_dropped_messages_by_ver.unset(r);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -23,11 +23,8 @@
|
||||
|
||||
#include "api.hh"
|
||||
|
||||
namespace netw { class messaging_service; }
|
||||
|
||||
namespace api {
|
||||
|
||||
void set_messaging_service(http_context& ctx, routes& r, sharded<netw::messaging_service>& ms);
|
||||
void unset_messaging_service(http_context& ctx, routes& r);
|
||||
void set_messaging_service(http_context& ctx, routes& r);
|
||||
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
#include "db/config.hh"
|
||||
#include "utils/histogram.hh"
|
||||
#include "database.hh"
|
||||
#include "seastar/core/scheduling_specific.hh"
|
||||
|
||||
namespace api {
|
||||
|
||||
@@ -35,70 +34,12 @@ namespace sp = httpd::storage_proxy_json;
|
||||
using proxy = service::storage_proxy;
|
||||
using namespace json;
|
||||
|
||||
|
||||
/**
|
||||
* This function implement a two dimentional map reduce where
|
||||
* the first level is a distributed storage_proxy class and the
|
||||
* second level is the stats per scheduling group class.
|
||||
* @param d - a reference to the storage_proxy distributed class.
|
||||
* @param mapper - the internal mapper that is used to map the internal
|
||||
* stat class into a value of type `V`.
|
||||
* @param reducer - the reducer that is used in both outer and inner
|
||||
* aggregations.
|
||||
* @param initial_value - the initial value to use for both aggregations
|
||||
* @return A future that resolves to the result of the aggregation.
|
||||
*/
|
||||
template<typename V, typename Reducer, typename InnerMapper>
|
||||
future<V> two_dimensional_map_reduce(distributed<service::storage_proxy>& d,
|
||||
InnerMapper mapper, Reducer reducer, V initial_value) {
|
||||
return d.map_reduce0( [mapper, reducer, initial_value] (const service::storage_proxy& sp) {
|
||||
return map_reduce_scheduling_group_specific<service::storage_proxy_stats::stats>(
|
||||
mapper, reducer, initial_value, sp.get_stats_key());
|
||||
}, initial_value, reducer);
|
||||
static future<utils::rate_moving_average> sum_timed_rate(distributed<proxy>& d, utils::timed_rate_moving_average proxy::stats::*f) {
|
||||
return d.map_reduce0([f](const proxy& p) {return (p.get_stats().*f).rate();}, utils::rate_moving_average(),
|
||||
std::plus<utils::rate_moving_average>());
|
||||
}
|
||||
|
||||
/**
|
||||
* This function implement a two dimentional map reduce where
|
||||
* the first level is a distributed storage_proxy class and the
|
||||
* second level is the stats per scheduling group class.
|
||||
* @param d - a reference to the storage_proxy distributed class.
|
||||
* @param f - a field pointer which is the implicit internal reducer.
|
||||
* @param reducer - the reducer that is used in both outer and inner
|
||||
* aggregations.
|
||||
* @param initial_value - the initial value to use for both aggregations* @return
|
||||
* @return A future that resolves to the result of the aggregation.
|
||||
*/
|
||||
template<typename V, typename Reducer, typename F>
|
||||
future<V> two_dimensional_map_reduce(distributed<service::storage_proxy>& d,
|
||||
V F::*f, Reducer reducer, V initial_value) {
|
||||
return two_dimensional_map_reduce(d, [f] (F& stats) {
|
||||
return stats.*f;
|
||||
}, reducer, initial_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* A partial Specialization of sum_stats for the storage proxy
|
||||
* case where the get stats function doesn't return a
|
||||
* stats object with fields but a per scheduling group
|
||||
* stats object, the name was also changed since functions
|
||||
* partial specialization is not supported in C++.
|
||||
*
|
||||
*/
|
||||
template<typename V, typename F>
|
||||
future<json::json_return_type> sum_stats_storage_proxy(distributed<proxy>& d, V F::*f) {
|
||||
return two_dimensional_map_reduce(d, [f] (F& stats) { return stats.*f; }, std::plus<V>(), V(0)).then([] (V val) {
|
||||
return make_ready_future<json::json_return_type>(val);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
static future<utils::rate_moving_average> sum_timed_rate(distributed<proxy>& d, utils::timed_rate_moving_average service::storage_proxy_stats::stats::*f) {
|
||||
return two_dimensional_map_reduce(d, [f] (service::storage_proxy_stats::stats& stats) {
|
||||
return (stats.*f).rate();
|
||||
}, std::plus<utils::rate_moving_average>(), utils::rate_moving_average());
|
||||
}
|
||||
|
||||
static future<json::json_return_type> sum_timed_rate_as_obj(distributed<proxy>& d, utils::timed_rate_moving_average service::storage_proxy_stats::stats::*f) {
|
||||
static future<json::json_return_type> sum_timed_rate_as_obj(distributed<proxy>& d, utils::timed_rate_moving_average proxy::stats::*f) {
|
||||
return sum_timed_rate(d, f).then([](const utils::rate_moving_average& val) {
|
||||
httpd::utils_json::rate_moving_average m;
|
||||
m = val;
|
||||
@@ -110,89 +51,29 @@ httpd::utils_json::rate_moving_average_and_histogram get_empty_moving_average()
|
||||
return timer_to_json(utils::rate_moving_average_and_histogram());
|
||||
}
|
||||
|
||||
static future<json::json_return_type> sum_timed_rate_as_long(distributed<proxy>& d, utils::timed_rate_moving_average service::storage_proxy_stats::stats::*f) {
|
||||
static future<json::json_return_type> sum_timed_rate_as_long(distributed<proxy>& d, utils::timed_rate_moving_average proxy::stats::*f) {
|
||||
return sum_timed_rate(d, f).then([](const utils::rate_moving_average& val) {
|
||||
return make_ready_future<json::json_return_type>(val.count);
|
||||
});
|
||||
}
|
||||
|
||||
utils_json::estimated_histogram time_to_json_histogram(const utils::time_estimated_histogram& val) {
|
||||
utils_json::estimated_histogram res;
|
||||
for (size_t i = 0; i < val.size(); i++) {
|
||||
res.buckets.push(val.get(i));
|
||||
res.bucket_offsets.push(val.get_bucket_lower_limit(i));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static future<json::json_return_type> sum_estimated_histogram(http_context& ctx, utils::time_estimated_histogram service::storage_proxy_stats::stats::*f) {
|
||||
|
||||
return two_dimensional_map_reduce(ctx.sp, f, utils::time_estimated_histogram_merge,
|
||||
utils::time_estimated_histogram()).then([](const utils::time_estimated_histogram& val) {
|
||||
return make_ready_future<json::json_return_type>(time_to_json_histogram(val));
|
||||
});
|
||||
}
|
||||
|
||||
static future<json::json_return_type> sum_estimated_histogram(http_context& ctx, utils::estimated_histogram service::storage_proxy_stats::stats::*f) {
|
||||
|
||||
return two_dimensional_map_reduce(ctx.sp, f, utils::estimated_histogram_merge,
|
||||
utils::estimated_histogram()).then([](const utils::estimated_histogram& val) {
|
||||
static future<json::json_return_type> sum_estimated_histogram(http_context& ctx, utils::estimated_histogram proxy::stats::*f) {
|
||||
return ctx.sp.map_reduce0([f](const proxy& p) {return p.get_stats().*f;}, utils::estimated_histogram(),
|
||||
utils::estimated_histogram_merge).then([](const utils::estimated_histogram& val) {
|
||||
utils_json::estimated_histogram res;
|
||||
res = val;
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
}
|
||||
|
||||
static future<json::json_return_type> total_latency(http_context& ctx, utils::timed_rate_moving_average_and_histogram service::storage_proxy_stats::stats::*f) {
|
||||
return two_dimensional_map_reduce(ctx.sp, [f] (service::storage_proxy_stats::stats& stats) {
|
||||
return (stats.*f).hist.mean * (stats.*f).hist.count;
|
||||
}, std::plus<double>(), 0.0).then([](double val) {
|
||||
static future<json::json_return_type> total_latency(http_context& ctx, utils::timed_rate_moving_average_and_histogram proxy::stats::*f) {
|
||||
return ctx.sp.map_reduce0([f](const proxy& p) {return (p.get_stats().*f).hist.mean * (p.get_stats().*f).hist.count;}, 0.0,
|
||||
std::plus<double>()).then([](double val) {
|
||||
int64_t res = val;
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A partial Specialization of sum_histogram_stats
|
||||
* for the storage proxy case where the get stats
|
||||
* function doesn't return a stats object with
|
||||
* fields but a per scheduling group stats object,
|
||||
* the name was also changed since function partial
|
||||
* specialization is not supported in C++.
|
||||
*/
|
||||
template<typename F>
|
||||
future<json::json_return_type>
|
||||
sum_histogram_stats_storage_proxy(distributed<proxy>& d,
|
||||
utils::timed_rate_moving_average_and_histogram F::*f) {
|
||||
return two_dimensional_map_reduce(d, [f] (service::storage_proxy_stats::stats& stats) {
|
||||
return (stats.*f).hist;
|
||||
}, std::plus<utils::ihistogram>(), utils::ihistogram()).
|
||||
then([](const utils::ihistogram& val) {
|
||||
return make_ready_future<json::json_return_type>(to_json(val));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A partial Specialization of sum_timer_stats for the
|
||||
* storage proxy case where the get stats function
|
||||
* doesn't return a stats object with fields but a
|
||||
* per scheduling group stats object, the name
|
||||
* was also changed since partial function specialization
|
||||
* is not supported in C++.
|
||||
*/
|
||||
template<typename F>
|
||||
future<json::json_return_type>
|
||||
sum_timer_stats_storage_proxy(distributed<proxy>& d,
|
||||
utils::timed_rate_moving_average_and_histogram F::*f) {
|
||||
|
||||
return two_dimensional_map_reduce(d, [f] (service::storage_proxy_stats::stats& stats) {
|
||||
return (stats.*f).rate();
|
||||
}, std::plus<utils::rate_moving_average_and_histogram>(),
|
||||
utils::rate_moving_average_and_histogram()).then([](const utils::rate_moving_average_and_histogram& val) {
|
||||
return make_ready_future<json::json_return_type>(timer_to_json(val));
|
||||
});
|
||||
}
|
||||
|
||||
void set_storage_proxy(http_context& ctx, routes& r) {
|
||||
sp::get_total_hints.set(r, [](std::unique_ptr<request> req) {
|
||||
//TBD
|
||||
@@ -201,39 +82,29 @@ void set_storage_proxy(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
sp::get_hinted_handoff_enabled.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
const auto& filter = service::get_storage_proxy().local().get_hints_host_filter();
|
||||
return make_ready_future<json::json_return_type>(!filter.is_disabled_for_all());
|
||||
auto enabled = ctx.db.local().get_config().hinted_handoff_enabled();
|
||||
return make_ready_future<json::json_return_type>(enabled);
|
||||
});
|
||||
|
||||
sp::set_hinted_handoff_enabled.set(r, [](std::unique_ptr<request> req) {
|
||||
//TBD
|
||||
unimplemented();
|
||||
auto enable = req->get_query_param("enable");
|
||||
auto filter = (enable == "true" || enable == "1")
|
||||
? db::hints::host_filter(db::hints::host_filter::enabled_for_all_tag {})
|
||||
: db::hints::host_filter(db::hints::host_filter::disabled_for_all_tag {});
|
||||
return service::get_storage_proxy().invoke_on_all([filter = std::move(filter)] (service::storage_proxy& sp) {
|
||||
return sp.change_hints_host_filter(filter);
|
||||
}).then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
|
||||
sp::get_hinted_handoff_enabled_by_dc.set(r, [](std::unique_ptr<request> req) {
|
||||
std::vector<sstring> res;
|
||||
const auto& filter = service::get_storage_proxy().local().get_hints_host_filter();
|
||||
const auto& dcs = filter.get_dcs();
|
||||
res.reserve(res.size());
|
||||
std::copy(dcs.begin(), dcs.end(), std::back_inserter(res));
|
||||
//TBD
|
||||
unimplemented();
|
||||
std::vector<sp::mapper_list> res;
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
|
||||
sp::set_hinted_handoff_enabled_by_dc_list.set(r, [](std::unique_ptr<request> req) {
|
||||
auto dcs = req->get_query_param("dcs");
|
||||
auto filter = db::hints::host_filter::parse_from_dc_list(std::move(dcs));
|
||||
return service::get_storage_proxy().invoke_on_all([filter = std::move(filter)] (service::storage_proxy& sp) {
|
||||
return sp.change_hints_host_filter(filter);
|
||||
}).then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
//TBD
|
||||
unimplemented();
|
||||
auto enable = req->get_query_param("dcs");
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
|
||||
sp::get_max_hint_window.set(r, [](std::unique_ptr<request> req) {
|
||||
@@ -352,15 +223,15 @@ void set_storage_proxy(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
sp::get_read_repair_attempted.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_stats_storage_proxy(ctx.sp, &service::storage_proxy_stats::stats::read_repair_attempts);
|
||||
return sum_stats(ctx.sp, &proxy::stats::read_repair_attempts);
|
||||
});
|
||||
|
||||
sp::get_read_repair_repaired_blocking.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_stats_storage_proxy(ctx.sp, &service::storage_proxy_stats::stats::read_repair_repaired_blocking);
|
||||
return sum_stats(ctx.sp, &proxy::stats::read_repair_repaired_blocking);
|
||||
});
|
||||
|
||||
sp::get_read_repair_repaired_background.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_stats_storage_proxy(ctx.sp, &service::storage_proxy_stats::stats::read_repair_repaired_background);
|
||||
return sum_stats(ctx.sp, &proxy::stats::read_repair_repaired_background);
|
||||
});
|
||||
|
||||
sp::get_schema_versions.set(r, [](std::unique_ptr<request> req) {
|
||||
@@ -404,10 +275,6 @@ void set_storage_proxy(http_context& ctx, routes& r) {
|
||||
return sum_stats(ctx.sp, &proxy::stats::cas_write_condition_not_met);
|
||||
});
|
||||
|
||||
sp::get_cas_write_metrics_failed_read_round_optimization.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_stats(ctx.sp, &proxy::stats::cas_failed_read_round_optimization);
|
||||
});
|
||||
|
||||
sp::get_cas_read_metrics_unfinished_commit.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_stats(ctx.sp, &proxy::stats::cas_read_unfinished_commit);
|
||||
});
|
||||
@@ -417,71 +284,71 @@ void set_storage_proxy(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
sp::get_read_metrics_timeouts.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timed_rate_as_long(ctx.sp, &service::storage_proxy_stats::stats::read_timeouts);
|
||||
return sum_timed_rate_as_long(ctx.sp, &proxy::stats::read_timeouts);
|
||||
});
|
||||
|
||||
sp::get_read_metrics_unavailables.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timed_rate_as_long(ctx.sp, &service::storage_proxy_stats::stats::read_unavailables);
|
||||
return sum_timed_rate_as_long(ctx.sp, &proxy::stats::read_unavailables);
|
||||
});
|
||||
|
||||
sp::get_range_metrics_timeouts.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timed_rate_as_long(ctx.sp, &service::storage_proxy_stats::stats::range_slice_timeouts);
|
||||
return sum_timed_rate_as_long(ctx.sp, &proxy::stats::range_slice_timeouts);
|
||||
});
|
||||
|
||||
sp::get_range_metrics_unavailables.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timed_rate_as_long(ctx.sp, &service::storage_proxy_stats::stats::range_slice_unavailables);
|
||||
return sum_timed_rate_as_long(ctx.sp, &proxy::stats::range_slice_unavailables);
|
||||
});
|
||||
|
||||
sp::get_write_metrics_timeouts.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timed_rate_as_long(ctx.sp, &service::storage_proxy_stats::stats::write_timeouts);
|
||||
return sum_timed_rate_as_long(ctx.sp, &proxy::stats::write_timeouts);
|
||||
});
|
||||
|
||||
sp::get_write_metrics_unavailables.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timed_rate_as_long(ctx.sp, &service::storage_proxy_stats::stats::write_unavailables);
|
||||
return sum_timed_rate_as_long(ctx.sp, &proxy::stats::write_unavailables);
|
||||
});
|
||||
|
||||
sp::get_read_metrics_timeouts_rates.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timed_rate_as_obj(ctx.sp, &service::storage_proxy_stats::stats::read_timeouts);
|
||||
return sum_timed_rate_as_obj(ctx.sp, &proxy::stats::read_timeouts);
|
||||
});
|
||||
|
||||
sp::get_read_metrics_unavailables_rates.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timed_rate_as_obj(ctx.sp, &service::storage_proxy_stats::stats::read_unavailables);
|
||||
return sum_timed_rate_as_obj(ctx.sp, &proxy::stats::read_unavailables);
|
||||
});
|
||||
|
||||
sp::get_range_metrics_timeouts_rates.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timed_rate_as_obj(ctx.sp, &service::storage_proxy_stats::stats::range_slice_timeouts);
|
||||
return sum_timed_rate_as_obj(ctx.sp, &proxy::stats::range_slice_timeouts);
|
||||
});
|
||||
|
||||
sp::get_range_metrics_unavailables_rates.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timed_rate_as_obj(ctx.sp, &service::storage_proxy_stats::stats::range_slice_unavailables);
|
||||
return sum_timed_rate_as_obj(ctx.sp, &proxy::stats::range_slice_unavailables);
|
||||
});
|
||||
|
||||
sp::get_write_metrics_timeouts_rates.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timed_rate_as_obj(ctx.sp, &service::storage_proxy_stats::stats::write_timeouts);
|
||||
return sum_timed_rate_as_obj(ctx.sp, &proxy::stats::write_timeouts);
|
||||
});
|
||||
|
||||
sp::get_write_metrics_unavailables_rates.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timed_rate_as_obj(ctx.sp, &service::storage_proxy_stats::stats::write_unavailables);
|
||||
return sum_timed_rate_as_obj(ctx.sp, &proxy::stats::write_unavailables);
|
||||
});
|
||||
|
||||
sp::get_range_metrics_latency_histogram_depricated.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_histogram_stats_storage_proxy(ctx.sp, &service::storage_proxy_stats::stats::range);
|
||||
return sum_histogram_stats(ctx.sp, &proxy::stats::range);
|
||||
});
|
||||
|
||||
sp::get_write_metrics_latency_histogram_depricated.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_histogram_stats_storage_proxy(ctx.sp, &service::storage_proxy_stats::stats::write);
|
||||
return sum_histogram_stats(ctx.sp, &proxy::stats::write);
|
||||
});
|
||||
|
||||
sp::get_read_metrics_latency_histogram_depricated.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_histogram_stats_storage_proxy(ctx.sp, &service::storage_proxy_stats::stats::read);
|
||||
return sum_histogram_stats(ctx.sp, &proxy::stats::read);
|
||||
});
|
||||
|
||||
sp::get_range_metrics_latency_histogram.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timer_stats_storage_proxy(ctx.sp, &service::storage_proxy_stats::stats::range);
|
||||
return sum_timer_stats(ctx.sp, &proxy::stats::range);
|
||||
});
|
||||
|
||||
sp::get_write_metrics_latency_histogram.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timer_stats_storage_proxy(ctx.sp, &service::storage_proxy_stats::stats::write);
|
||||
return sum_timer_stats(ctx.sp, &proxy::stats::write);
|
||||
});
|
||||
sp::get_cas_write_metrics_latency_histogram.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timer_stats(ctx.sp, &proxy::stats::cas_write);
|
||||
@@ -500,30 +367,30 @@ void set_storage_proxy(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
sp::get_read_metrics_latency_histogram.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timer_stats_storage_proxy(ctx.sp, &service::storage_proxy_stats::stats::read);
|
||||
return sum_timer_stats(ctx.sp, &proxy::stats::read);
|
||||
});
|
||||
|
||||
sp::get_read_estimated_histogram.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_estimated_histogram(ctx, &service::storage_proxy_stats::stats::estimated_read);
|
||||
return sum_estimated_histogram(ctx, &proxy::stats::estimated_read);
|
||||
});
|
||||
|
||||
sp::get_read_latency.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return total_latency(ctx, &service::storage_proxy_stats::stats::read);
|
||||
return total_latency(ctx, &proxy::stats::read);
|
||||
});
|
||||
sp::get_write_estimated_histogram.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_estimated_histogram(ctx, &service::storage_proxy_stats::stats::estimated_write);
|
||||
return sum_estimated_histogram(ctx, &proxy::stats::estimated_write);
|
||||
});
|
||||
|
||||
sp::get_write_latency.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return total_latency(ctx, &service::storage_proxy_stats::stats::write);
|
||||
return total_latency(ctx, &proxy::stats::write);
|
||||
});
|
||||
|
||||
sp::get_range_estimated_histogram.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timer_stats_storage_proxy(ctx.sp, &service::storage_proxy_stats::stats::range);
|
||||
return sum_timer_stats(ctx.sp, &proxy::stats::range);
|
||||
});
|
||||
|
||||
sp::get_range_latency.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return total_latency(ctx, &service::storage_proxy_stats::stats::range);
|
||||
return total_latency(ctx, &proxy::stats::range);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#include "storage_service.hh"
|
||||
#include "api/api-doc/storage_service.json.hh"
|
||||
#include "db/config.hh"
|
||||
#include "db/schema_tables.hh"
|
||||
#include <optional>
|
||||
#include <time.h>
|
||||
#include <boost/range/adaptor/map.hpp>
|
||||
@@ -42,17 +41,11 @@
|
||||
#include "sstables/sstables.hh"
|
||||
#include "database.hh"
|
||||
#include "db/extensions.hh"
|
||||
#include "db/snapshot-ctl.hh"
|
||||
#include "transport/controller.hh"
|
||||
#include "thrift/controller.hh"
|
||||
#include "locator/token_metadata.hh"
|
||||
|
||||
sstables::sstable::version_types get_highest_supported_format();
|
||||
|
||||
namespace api {
|
||||
|
||||
const locator::token_metadata& http_context::get_token_metadata() {
|
||||
return *shared_token_metadata.local().get();
|
||||
}
|
||||
|
||||
namespace ss = httpd::storage_service_json;
|
||||
using namespace json;
|
||||
|
||||
@@ -81,195 +74,35 @@ static ss::token_range token_range_endpoints_to_json(const dht::token_range_endp
|
||||
return r;
|
||||
}
|
||||
|
||||
using ks_cf_func = std::function<future<json::json_return_type>(http_context&, std::unique_ptr<request>, sstring, std::vector<sstring>)>;
|
||||
|
||||
static auto wrap_ks_cf(http_context &ctx, ks_cf_func f) {
|
||||
return [&ctx, f = std::move(f)](std::unique_ptr<request> req) {
|
||||
auto keyspace = validate_keyspace(ctx, req->param);
|
||||
auto column_families = split_cf(req->get_query_param("cf"));
|
||||
if (column_families.empty()) {
|
||||
column_families = map_keys(ctx.db.local().find_keyspace(keyspace).metadata().get()->cf_meta_data());
|
||||
}
|
||||
return f(ctx, std::move(req), std::move(keyspace), std::move(column_families));
|
||||
};
|
||||
}
|
||||
|
||||
future<json::json_return_type> set_tables_autocompaction(http_context& ctx, const sstring &keyspace, std::vector<sstring> tables, bool enabled) {
|
||||
if (tables.empty()) {
|
||||
tables = map_keys(ctx.db.local().find_keyspace(keyspace).metadata().get()->cf_meta_data());
|
||||
}
|
||||
|
||||
return service::get_local_storage_service().set_tables_autocompaction(keyspace, tables, enabled).then([]{
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
}
|
||||
|
||||
void set_transport_controller(http_context& ctx, routes& r, cql_transport::controller& ctl) {
|
||||
ss::start_native_transport.set(r, [&ctl](std::unique_ptr<request> req) {
|
||||
return ctl.start_server().then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
ss::stop_native_transport.set(r, [&ctl](std::unique_ptr<request> req) {
|
||||
return ctl.stop_server().then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
ss::is_native_transport_running.set(r, [&ctl] (std::unique_ptr<request> req) {
|
||||
return ctl.is_server_running().then([] (bool running) {
|
||||
return make_ready_future<json::json_return_type>(running);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void unset_transport_controller(http_context& ctx, routes& r) {
|
||||
ss::start_native_transport.unset(r);
|
||||
ss::stop_native_transport.unset(r);
|
||||
ss::is_native_transport_running.unset(r);
|
||||
}
|
||||
|
||||
void set_rpc_controller(http_context& ctx, routes& r, thrift_controller& ctl) {
|
||||
ss::stop_rpc_server.set(r, [&ctl](std::unique_ptr<request> req) {
|
||||
return ctl.stop_server().then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
ss::start_rpc_server.set(r, [&ctl](std::unique_ptr<request> req) {
|
||||
return ctl.start_server().then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
ss::is_rpc_server_running.set(r, [&ctl] (std::unique_ptr<request> req) {
|
||||
return ctl.is_server_running().then([] (bool running) {
|
||||
return make_ready_future<json::json_return_type>(running);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void unset_rpc_controller(http_context& ctx, routes& r) {
|
||||
ss::stop_rpc_server.unset(r);
|
||||
ss::start_rpc_server.unset(r);
|
||||
ss::is_rpc_server_running.unset(r);
|
||||
}
|
||||
|
||||
void set_repair(http_context& ctx, routes& r, sharded<netw::messaging_service>& ms) {
|
||||
ss::repair_async.set(r, [&ctx, &ms](std::unique_ptr<request> req) {
|
||||
static std::vector<sstring> options = {"primaryRange", "parallelism", "incremental",
|
||||
"jobThreads", "ranges", "columnFamilies", "dataCenters", "hosts", "trace",
|
||||
"startToken", "endToken" };
|
||||
std::unordered_map<sstring, sstring> options_map;
|
||||
for (auto o : options) {
|
||||
auto s = req->get_query_param(o);
|
||||
if (s != "") {
|
||||
options_map[o] = s;
|
||||
}
|
||||
}
|
||||
|
||||
// The repair process is asynchronous: repair_start only starts it and
|
||||
// returns immediately, not waiting for the repair to finish. The user
|
||||
// then has other mechanisms to track the ongoing repair's progress,
|
||||
// or stop it.
|
||||
return repair_start(ctx.db, ms, validate_keyspace(ctx, req->param),
|
||||
options_map).then([] (int i) {
|
||||
return make_ready_future<json::json_return_type>(i);
|
||||
});
|
||||
});
|
||||
|
||||
ss::get_active_repair_async.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return get_active_repairs(ctx.db).then([] (std::vector<int> res){
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
});
|
||||
|
||||
ss::repair_async_status.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return repair_get_status(ctx.db, boost::lexical_cast<int>( req->get_query_param("id")))
|
||||
.then_wrapped([] (future<repair_status>&& fut) {
|
||||
ss::ns_repair_async_status::return_type_wrapper res;
|
||||
try {
|
||||
res = fut.get0();
|
||||
} catch(std::runtime_error& e) {
|
||||
throw httpd::bad_param_exception(e.what());
|
||||
}
|
||||
return make_ready_future<json::json_return_type>(json::json_return_type(res));
|
||||
});
|
||||
});
|
||||
|
||||
ss::repair_await_completion.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
int id;
|
||||
using clock = std::chrono::steady_clock;
|
||||
clock::time_point expire;
|
||||
try {
|
||||
id = boost::lexical_cast<int>(req->get_query_param("id"));
|
||||
// If timeout is not provided, it means no timeout.
|
||||
sstring s = req->get_query_param("timeout");
|
||||
int64_t timeout = s.empty() ? int64_t(-1) : boost::lexical_cast<int64_t>(s);
|
||||
if (timeout < 0 && timeout != -1) {
|
||||
return make_exception_future<json::json_return_type>(
|
||||
httpd::bad_param_exception("timeout can only be -1 (means no timeout) or non negative integer"));
|
||||
}
|
||||
if (timeout < 0) {
|
||||
expire = clock::time_point::max();
|
||||
} else {
|
||||
expire = clock::now() + std::chrono::seconds(timeout);
|
||||
}
|
||||
} catch (std::exception& e) {
|
||||
return make_exception_future<json::json_return_type>(httpd::bad_param_exception(e.what()));
|
||||
}
|
||||
return repair_await_completion(ctx.db, id, expire)
|
||||
.then_wrapped([] (future<repair_status>&& fut) {
|
||||
ss::ns_repair_async_status::return_type_wrapper res;
|
||||
try {
|
||||
res = fut.get0();
|
||||
} catch (std::exception& e) {
|
||||
return make_exception_future<json::json_return_type>(httpd::server_error_exception(e.what()));
|
||||
}
|
||||
return make_ready_future<json::json_return_type>(json::json_return_type(res));
|
||||
});
|
||||
});
|
||||
|
||||
ss::force_terminate_all_repair_sessions.set(r, [](std::unique_ptr<request> req) {
|
||||
return repair_abort_all(service::get_local_storage_service().db()).then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
ss::force_terminate_all_repair_sessions_new.set(r, [](std::unique_ptr<request> req) {
|
||||
return repair_abort_all(service::get_local_storage_service().db()).then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
void unset_repair(http_context& ctx, routes& r) {
|
||||
ss::repair_async.unset(r);
|
||||
ss::get_active_repair_async.unset(r);
|
||||
ss::repair_async_status.unset(r);
|
||||
ss::repair_await_completion.unset(r);
|
||||
ss::force_terminate_all_repair_sessions.unset(r);
|
||||
ss::force_terminate_all_repair_sessions_new.unset(r);
|
||||
}
|
||||
|
||||
void set_storage_service(http_context& ctx, routes& r) {
|
||||
using ks_cf_func = std::function<future<json::json_return_type>(std::unique_ptr<request>, sstring, std::vector<sstring>)>;
|
||||
|
||||
auto wrap_ks_cf = [&ctx](ks_cf_func f) {
|
||||
return [&ctx, f = std::move(f)](std::unique_ptr<request> req) {
|
||||
auto keyspace = validate_keyspace(ctx, req->param);
|
||||
auto column_families = split_cf(req->get_query_param("cf"));
|
||||
if (column_families.empty()) {
|
||||
column_families = map_keys(ctx.db.local().find_keyspace(keyspace).metadata().get()->cf_meta_data());
|
||||
}
|
||||
return f(std::move(req), std::move(keyspace), std::move(column_families));
|
||||
};
|
||||
};
|
||||
|
||||
ss::local_hostid.set(r, [](std::unique_ptr<request> req) {
|
||||
return db::system_keyspace::get_local_host_id().then([](const utils::UUID& id) {
|
||||
return make_ready_future<json::json_return_type>(id.to_sstring());
|
||||
});
|
||||
});
|
||||
|
||||
ss::get_tokens.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return make_ready_future<json::json_return_type>(stream_range_as_array(ctx.get_token_metadata().sorted_tokens(), [](const dht::token& i) {
|
||||
ss::get_tokens.set(r, [] (std::unique_ptr<request> req) {
|
||||
return make_ready_future<json::json_return_type>(stream_range_as_array(service::get_local_storage_service().get_token_metadata().sorted_tokens(), [](const dht::token& i) {
|
||||
return boost::lexical_cast<std::string>(i);
|
||||
}));
|
||||
});
|
||||
|
||||
ss::get_node_tokens.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
ss::get_node_tokens.set(r, [] (std::unique_ptr<request> req) {
|
||||
gms::inet_address addr(req->param["endpoint"]);
|
||||
return make_ready_future<json::json_return_type>(stream_range_as_array(ctx.get_token_metadata().get_tokens(addr), [](const dht::token& i) {
|
||||
return make_ready_future<json::json_return_type>(stream_range_as_array(service::get_local_storage_service().get_token_metadata().get_tokens(addr), [](const dht::token& i) {
|
||||
return boost::lexical_cast<std::string>(i);
|
||||
}));
|
||||
});
|
||||
@@ -287,8 +120,8 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
}));
|
||||
});
|
||||
|
||||
ss::get_leaving_nodes.set(r, [&ctx](const_req req) {
|
||||
return container_to_vec(ctx.get_token_metadata().get_leaving_endpoints());
|
||||
ss::get_leaving_nodes.set(r, [](const_req req) {
|
||||
return container_to_vec(service::get_local_storage_service().get_token_metadata().get_leaving_endpoints());
|
||||
});
|
||||
|
||||
ss::get_moving_nodes.set(r, [](const_req req) {
|
||||
@@ -296,8 +129,8 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
return container_to_vec(addr);
|
||||
});
|
||||
|
||||
ss::get_joining_nodes.set(r, [&ctx](const_req req) {
|
||||
auto points = ctx.get_token_metadata().get_bootstrap_tokens();
|
||||
ss::get_joining_nodes.set(r, [](const_req req) {
|
||||
auto points = service::get_local_storage_service().get_token_metadata().get_bootstrap_tokens();
|
||||
std::unordered_set<sstring> addr;
|
||||
for (auto i: points) {
|
||||
addr.insert(boost::lexical_cast<std::string>(i.second));
|
||||
@@ -325,26 +158,11 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
ss::get_range_to_endpoint_map.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
//TBD
|
||||
unimplemented();
|
||||
auto keyspace = validate_keyspace(ctx, req->param);
|
||||
std::vector<ss::maplist_mapper> res;
|
||||
return make_ready_future<json::json_return_type>(stream_range_as_array(service::get_local_storage_service().get_range_to_address_map(keyspace),
|
||||
[](const std::pair<dht::token_range, std::vector<gms::inet_address>>& entry){
|
||||
ss::maplist_mapper m;
|
||||
if (entry.first.start()) {
|
||||
m.key.push(entry.first.start().value().value().to_sstring());
|
||||
} else {
|
||||
m.key.push("");
|
||||
}
|
||||
if (entry.first.end()) {
|
||||
m.key.push(entry.first.end().value().value().to_sstring());
|
||||
} else {
|
||||
m.key.push("");
|
||||
}
|
||||
for (const gms::inet_address& address : entry.second) {
|
||||
m.value.push(address.to_sstring());
|
||||
}
|
||||
return m;
|
||||
}));
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
|
||||
ss::get_pending_range_to_endpoint_map.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
@@ -364,9 +182,10 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
return make_ready_future<json::json_return_type>(stream_range_as_array(service::get_local_storage_service().describe_ring(keyspace), token_range_endpoints_to_json));
|
||||
});
|
||||
|
||||
ss::get_host_id_map.set(r, [&ctx](const_req req) {
|
||||
ss::get_host_id_map.set(r, [](const_req req) {
|
||||
std::vector<ss::mapper> res;
|
||||
return map_to_key_value(ctx.get_token_metadata().get_endpoint_to_host_id_map_for_reading(), res);
|
||||
return map_to_key_value(service::get_local_storage_service().
|
||||
get_token_metadata().get_endpoint_to_host_id_map_for_reading(), res);
|
||||
});
|
||||
|
||||
ss::get_load.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
@@ -399,12 +218,67 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
req.get_query_param("key")));
|
||||
});
|
||||
|
||||
ss::cdc_streams_check_and_repair.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return service::get_local_storage_service().check_and_repair_cdc_streams().then([] {
|
||||
ss::get_snapshot_details.set(r, [](std::unique_ptr<request> req) {
|
||||
return service::get_local_storage_service().get_snapshot_details().then([] (auto result) {
|
||||
std::vector<ss::snapshots> res;
|
||||
for (auto& map: result) {
|
||||
ss::snapshots all_snapshots;
|
||||
all_snapshots.key = map.first;
|
||||
|
||||
std::vector<ss::snapshot> snapshot;
|
||||
for (auto& cf: map.second) {
|
||||
ss::snapshot s;
|
||||
s.ks = cf.ks;
|
||||
s.cf = cf.cf;
|
||||
s.live = cf.live;
|
||||
s.total = cf.total;
|
||||
snapshot.push_back(std::move(s));
|
||||
}
|
||||
all_snapshots.value = std::move(snapshot);
|
||||
res.push_back(std::move(all_snapshots));
|
||||
}
|
||||
return make_ready_future<json::json_return_type>(std::move(res));
|
||||
});
|
||||
});
|
||||
|
||||
ss::take_snapshot.set(r, [](std::unique_ptr<request> req) {
|
||||
auto tag = req->get_query_param("tag");
|
||||
auto column_family = req->get_query_param("cf");
|
||||
|
||||
std::vector<sstring> keynames = split(req->get_query_param("kn"), ",");
|
||||
|
||||
auto resp = make_ready_future<>();
|
||||
if (column_family.empty()) {
|
||||
resp = service::get_local_storage_service().take_snapshot(tag, keynames);
|
||||
} else {
|
||||
if (keynames.empty()) {
|
||||
throw httpd::bad_param_exception("The keyspace of column families must be specified");
|
||||
}
|
||||
if (keynames.size() > 1) {
|
||||
throw httpd::bad_param_exception("Only one keyspace allowed when specifying a column family");
|
||||
}
|
||||
resp = service::get_local_storage_service().take_column_family_snapshot(keynames[0], column_family, tag);
|
||||
}
|
||||
return resp.then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
ss::del_snapshot.set(r, [](std::unique_ptr<request> req) {
|
||||
auto tag = req->get_query_param("tag");
|
||||
|
||||
std::vector<sstring> keynames = split(req->get_query_param("kn"), ",");
|
||||
return service::get_local_storage_service().clear_snapshot(tag, keynames).then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
ss::true_snapshots_size.set(r, [](std::unique_ptr<request> req) {
|
||||
return service::get_local_storage_service().true_snapshots_size().then([] (int64_t size) {
|
||||
return make_ready_future<json::json_return_type>(size);
|
||||
});
|
||||
});
|
||||
|
||||
ss::force_keyspace_compaction.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
auto keyspace = validate_keyspace(ctx, req->param);
|
||||
auto column_families = split_cf(req->get_query_param("cf"));
|
||||
@@ -442,8 +316,8 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
for (auto cf : column_families) {
|
||||
column_families_vec.push_back(&db.find_column_family(keyspace, cf));
|
||||
}
|
||||
return parallel_for_each(column_families_vec, [&cm, &db] (column_family* cf) {
|
||||
return cm.perform_cleanup(db, cf);
|
||||
return parallel_for_each(column_families_vec, [&cm] (column_family* cf) {
|
||||
return cm.perform_cleanup(cf);
|
||||
});
|
||||
}).then([]{
|
||||
return make_ready_future<json::json_return_type>(0);
|
||||
@@ -451,14 +325,39 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
});
|
||||
});
|
||||
|
||||
ss::upgrade_sstables.set(r, wrap_ks_cf(ctx, [] (http_context& ctx, std::unique_ptr<request> req, sstring keyspace, std::vector<sstring> column_families) {
|
||||
ss::scrub.set(r, wrap_ks_cf([&ctx](std::unique_ptr<request> req, sstring keyspace, std::vector<sstring> column_families) {
|
||||
// TODO: respect this
|
||||
auto skip_corrupted = req->get_query_param("skip_corrupted");
|
||||
|
||||
auto f = make_ready_future<>();
|
||||
if (!req_param<bool>(*req, "disable_snapshot", false)) {
|
||||
auto tag = format("pre-scrub-{:d}", db_clock::now().time_since_epoch().count());
|
||||
f = parallel_for_each(column_families, [keyspace, tag](sstring cf) {
|
||||
return service::get_local_storage_service().take_column_family_snapshot(keyspace, cf, tag);
|
||||
});
|
||||
}
|
||||
|
||||
return f.then([&ctx, keyspace, column_families] {
|
||||
return ctx.db.invoke_on_all([=] (database& db) {
|
||||
return do_for_each(column_families, [=, &db](sstring cfname) {
|
||||
auto& cm = db.get_compaction_manager();
|
||||
auto& cf = db.find_column_family(keyspace, cfname);
|
||||
return cm.perform_sstable_scrub(&cf);
|
||||
});
|
||||
});
|
||||
}).then([]{
|
||||
return make_ready_future<json::json_return_type>(0);
|
||||
});
|
||||
}));
|
||||
|
||||
ss::upgrade_sstables.set(r, wrap_ks_cf([&ctx](std::unique_ptr<request> req, sstring keyspace, std::vector<sstring> column_families) {
|
||||
bool exclude_current_version = req_param<bool>(*req, "exclude_current_version", false);
|
||||
|
||||
return ctx.db.invoke_on_all([=] (database& db) {
|
||||
return do_for_each(column_families, [=, &db](sstring cfname) {
|
||||
auto& cm = db.get_compaction_manager();
|
||||
auto& cf = db.find_column_family(keyspace, cfname);
|
||||
return cm.perform_sstable_upgrade(db, &cf, exclude_current_version);
|
||||
return cm.perform_sstable_upgrade(&cf, exclude_current_version);
|
||||
});
|
||||
}).then([]{
|
||||
return make_ready_future<json::json_return_type>(0);
|
||||
@@ -481,6 +380,59 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
|
||||
ss::repair_async.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
static std::vector<sstring> options = {"primaryRange", "parallelism", "incremental",
|
||||
"jobThreads", "ranges", "columnFamilies", "dataCenters", "hosts", "trace",
|
||||
"startToken", "endToken" };
|
||||
std::unordered_map<sstring, sstring> options_map;
|
||||
for (auto o : options) {
|
||||
auto s = req->get_query_param(o);
|
||||
if (s != "") {
|
||||
options_map[o] = s;
|
||||
}
|
||||
}
|
||||
|
||||
// The repair process is asynchronous: repair_start only starts it and
|
||||
// returns immediately, not waiting for the repair to finish. The user
|
||||
// then has other mechanisms to track the ongoing repair's progress,
|
||||
// or stop it.
|
||||
return repair_start(ctx.db, validate_keyspace(ctx, req->param),
|
||||
options_map).then([] (int i) {
|
||||
return make_ready_future<json::json_return_type>(i);
|
||||
});
|
||||
});
|
||||
|
||||
ss::get_active_repair_async.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return get_active_repairs(ctx.db).then([] (std::vector<int> res){
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
});
|
||||
|
||||
ss::repair_async_status.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return repair_get_status(ctx.db, boost::lexical_cast<int>( req->get_query_param("id")))
|
||||
.then_wrapped([] (future<repair_status>&& fut) {
|
||||
ss::ns_repair_async_status::return_type_wrapper res;
|
||||
try {
|
||||
res = fut.get0();
|
||||
} catch(std::runtime_error& e) {
|
||||
throw httpd::bad_param_exception(e.what());
|
||||
}
|
||||
return make_ready_future<json::json_return_type>(json::json_return_type(res));
|
||||
});
|
||||
});
|
||||
|
||||
ss::force_terminate_all_repair_sessions.set(r, [](std::unique_ptr<request> req) {
|
||||
return repair_abort_all(service::get_local_storage_service().db()).then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
ss::force_terminate_all_repair_sessions_new.set(r, [](std::unique_ptr<request> req) {
|
||||
return repair_abort_all(service::get_local_storage_service().db()).then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
ss::decommission.set(r, [](std::unique_ptr<request> req) {
|
||||
return service::get_local_storage_service().decommission().then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
@@ -616,6 +568,42 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
});
|
||||
});
|
||||
|
||||
ss::stop_rpc_server.set(r, [](std::unique_ptr<request> req) {
|
||||
return service::get_local_storage_service().stop_rpc_server().then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
ss::start_rpc_server.set(r, [](std::unique_ptr<request> req) {
|
||||
return service::get_local_storage_service().start_rpc_server().then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
ss::is_rpc_server_running.set(r, [] (std::unique_ptr<request> req) {
|
||||
return service::get_local_storage_service().is_rpc_server_running().then([] (bool running) {
|
||||
return make_ready_future<json::json_return_type>(running);
|
||||
});
|
||||
});
|
||||
|
||||
ss::start_native_transport.set(r, [](std::unique_ptr<request> req) {
|
||||
return service::get_local_storage_service().start_native_transport().then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
ss::stop_native_transport.set(r, [](std::unique_ptr<request> req) {
|
||||
return service::get_local_storage_service().stop_native_transport().then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
ss::is_native_transport_running.set(r, [] (std::unique_ptr<request> req) {
|
||||
return service::get_local_storage_service().is_native_transport_running().then([] (bool running) {
|
||||
return make_ready_future<json::json_return_type>(running);
|
||||
});
|
||||
});
|
||||
|
||||
ss::join_ring.set(r, [](std::unique_ptr<request> req) {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
@@ -738,17 +726,14 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
ss::reset_local_schema.set(r, [](std::unique_ptr<request> req) {
|
||||
// FIXME: We should truncate schema tables if more than one node in the cluster.
|
||||
auto& sp = service::get_storage_proxy();
|
||||
auto& fs = service::get_local_storage_service().features();
|
||||
return db::schema_tables::recalculate_schema_version(sp, fs).then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
//TBD
|
||||
unimplemented();
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
|
||||
ss::set_trace_probability.set(r, [](std::unique_ptr<request> req) {
|
||||
auto probability = req->get_query_param("probability");
|
||||
return futurize_invoke([probability] {
|
||||
return futurize<json::json_return_type>::apply([probability] {
|
||||
double real_prob = std::stod(probability.c_str());
|
||||
return tracing::tracing::tracing_instance().invoke_on_all([real_prob] (auto& local_tracing) {
|
||||
local_tracing.set_trace_probability(real_prob);
|
||||
@@ -803,17 +788,19 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
ss::enable_auto_compaction.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
//TBD
|
||||
unimplemented();
|
||||
auto keyspace = validate_keyspace(ctx, req->param);
|
||||
auto tables = split_cf(req->get_query_param("cf"));
|
||||
|
||||
return set_tables_autocompaction(ctx, keyspace, tables, true);
|
||||
auto column_family = req->get_query_param("cf");
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
|
||||
ss::disable_auto_compaction.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
//TBD
|
||||
unimplemented();
|
||||
auto keyspace = validate_keyspace(ctx, req->param);
|
||||
auto tables = split_cf(req->get_query_param("cf"));
|
||||
|
||||
return set_tables_autocompaction(ctx, keyspace, tables, false);
|
||||
auto column_family = req->get_query_param("cf");
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
|
||||
ss::deliver_hints.set(r, [](std::unique_ptr<request> req) {
|
||||
@@ -991,7 +978,7 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
e.value = p.second;
|
||||
nm.attributes.push(std::move(e));
|
||||
}
|
||||
if (!cp->options().contains(compression_parameters::SSTABLE_COMPRESSION)) {
|
||||
if (!cp->options().count(compression_parameters::SSTABLE_COMPRESSION)) {
|
||||
ss::mapper e;
|
||||
e.key = compression_parameters::SSTABLE_COMPRESSION;
|
||||
e.value = cp->name();
|
||||
@@ -1049,114 +1036,4 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
|
||||
}
|
||||
|
||||
void set_snapshot(http_context& ctx, routes& r, sharded<db::snapshot_ctl>& snap_ctl) {
|
||||
ss::get_snapshot_details.set(r, [&snap_ctl](std::unique_ptr<request> req) {
|
||||
return snap_ctl.local().get_snapshot_details().then([] (std::unordered_map<sstring, std::vector<db::snapshot_ctl::snapshot_details>>&& result) {
|
||||
std::function<future<>(output_stream<char>&&)> f = [result = std::move(result)](output_stream<char>&& s) {
|
||||
return do_with(output_stream<char>(std::move(s)), true, [&result] (output_stream<char>& s, bool& first){
|
||||
return s.write("[").then([&s, &first, &result] {
|
||||
return do_for_each(result, [&s, &first](std::tuple<sstring, std::vector<db::snapshot_ctl::snapshot_details>>&& map){
|
||||
return do_with(ss::snapshots(), [&s, &first, &map](ss::snapshots& all_snapshots) {
|
||||
all_snapshots.key = std::get<0>(map);
|
||||
future<> f = first ? make_ready_future<>() : s.write(", ");
|
||||
first = false;
|
||||
std::vector<ss::snapshot> snapshot;
|
||||
for (auto& cf: std::get<1>(map)) {
|
||||
ss::snapshot snp;
|
||||
snp.ks = cf.ks;
|
||||
snp.cf = cf.cf;
|
||||
snp.live = cf.live;
|
||||
snp.total = cf.total;
|
||||
snapshot.push_back(std::move(snp));
|
||||
}
|
||||
all_snapshots.value = std::move(snapshot);
|
||||
return f.then([&s, &all_snapshots] {
|
||||
return all_snapshots.write(s);
|
||||
});
|
||||
});
|
||||
});
|
||||
}).then([&s] {
|
||||
return s.write("]").then([&s] {
|
||||
return s.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return make_ready_future<json::json_return_type>(std::move(f));
|
||||
});
|
||||
});
|
||||
|
||||
ss::take_snapshot.set(r, [&snap_ctl](std::unique_ptr<request> req) {
|
||||
auto tag = req->get_query_param("tag");
|
||||
auto column_families = split(req->get_query_param("cf"), ",");
|
||||
|
||||
std::vector<sstring> keynames = split(req->get_query_param("kn"), ",");
|
||||
|
||||
auto resp = make_ready_future<>();
|
||||
if (column_families.empty()) {
|
||||
resp = snap_ctl.local().take_snapshot(tag, keynames);
|
||||
} else {
|
||||
if (keynames.empty()) {
|
||||
throw httpd::bad_param_exception("The keyspace of column families must be specified");
|
||||
}
|
||||
if (keynames.size() > 1) {
|
||||
throw httpd::bad_param_exception("Only one keyspace allowed when specifying a column family");
|
||||
}
|
||||
resp = snap_ctl.local().take_column_family_snapshot(keynames[0], column_families, tag);
|
||||
}
|
||||
return resp.then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
ss::del_snapshot.set(r, [&snap_ctl](std::unique_ptr<request> req) {
|
||||
auto tag = req->get_query_param("tag");
|
||||
auto column_family = req->get_query_param("cf");
|
||||
|
||||
std::vector<sstring> keynames = split(req->get_query_param("kn"), ",");
|
||||
return snap_ctl.local().clear_snapshot(tag, keynames, column_family).then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
ss::true_snapshots_size.set(r, [&snap_ctl](std::unique_ptr<request> req) {
|
||||
return snap_ctl.local().true_snapshots_size().then([] (int64_t size) {
|
||||
return make_ready_future<json::json_return_type>(size);
|
||||
});
|
||||
});
|
||||
|
||||
ss::scrub.set(r, wrap_ks_cf(ctx, [&snap_ctl] (http_context& ctx, std::unique_ptr<request> req, sstring keyspace, std::vector<sstring> column_families) {
|
||||
const auto skip_corrupted = req_param<bool>(*req, "skip_corrupted", false);
|
||||
|
||||
auto f = make_ready_future<>();
|
||||
if (!req_param<bool>(*req, "disable_snapshot", false)) {
|
||||
auto tag = format("pre-scrub-{:d}", db_clock::now().time_since_epoch().count());
|
||||
f = parallel_for_each(column_families, [&snap_ctl, keyspace, tag](sstring cf) {
|
||||
return snap_ctl.local().take_column_family_snapshot(keyspace, cf, tag);
|
||||
});
|
||||
}
|
||||
|
||||
return f.then([&ctx, keyspace, column_families, skip_corrupted] {
|
||||
return ctx.db.invoke_on_all([=] (database& db) {
|
||||
return do_for_each(column_families, [=, &db](sstring cfname) {
|
||||
auto& cm = db.get_compaction_manager();
|
||||
auto& cf = db.find_column_family(keyspace, cfname);
|
||||
return cm.perform_sstable_scrub(&cf, skip_corrupted);
|
||||
});
|
||||
});
|
||||
}).then([]{
|
||||
return make_ready_future<json::json_return_type>(0);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
void unset_snapshot(http_context& ctx, routes& r) {
|
||||
ss::get_snapshot_details.unset(r);
|
||||
ss::take_snapshot.unset(r);
|
||||
ss::del_snapshot.unset(r);
|
||||
ss::true_snapshots_size.unset(r);
|
||||
ss::scrub.unset(r);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,24 +21,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <seastar/core/sharded.hh>
|
||||
#include "api.hh"
|
||||
|
||||
namespace cql_transport { class controller; }
|
||||
class thrift_controller;
|
||||
namespace db { class snapshot_ctl; }
|
||||
namespace netw { class messaging_service; }
|
||||
|
||||
namespace api {
|
||||
|
||||
void set_storage_service(http_context& ctx, routes& r);
|
||||
void set_repair(http_context& ctx, routes& r, sharded<netw::messaging_service>& ms);
|
||||
void unset_repair(http_context& ctx, routes& r);
|
||||
void set_transport_controller(http_context& ctx, routes& r, cql_transport::controller& ctl);
|
||||
void unset_transport_controller(http_context& ctx, routes& r);
|
||||
void set_rpc_controller(http_context& ctx, routes& r, thrift_controller& ctl);
|
||||
void unset_rpc_controller(http_context& ctx, routes& r);
|
||||
void set_snapshot(http_context& ctx, routes& r, sharded<db::snapshot_ctl>& snap_ctl);
|
||||
void unset_snapshot(http_context& ctx, routes& r);
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#include "api/api-doc/system.json.hh"
|
||||
#include "api/api.hh"
|
||||
|
||||
#include <seastar/core/reactor.hh>
|
||||
#include <seastar/http/exception.hh>
|
||||
#include "log.hh"
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ size_t atomic_cell_or_collection::external_memory_usage(const abstract_type& t)
|
||||
external_value_size = cell_view.value_size();
|
||||
}
|
||||
// Add overhead of chunk headers. The last one is a special case.
|
||||
external_value_size += (external_value_size - 1) / data::cell::effective_external_chunk_length * data::cell::external_chunk_overhead;
|
||||
external_value_size += (external_value_size - 1) / data::cell::maximum_external_chunk_length * data::cell::external_chunk_overhead;
|
||||
external_value_size += data::cell::external_last_chunk_overhead;
|
||||
}
|
||||
return data::cell::structure::serialized_object_size(_data.get(), ctx)
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <seastar/net//byteorder.hh>
|
||||
#include <cstdint>
|
||||
#include <iosfwd>
|
||||
#include <seastar/util/gcc6-concepts.hh>
|
||||
#include "data/cell.hh"
|
||||
#include "data/schema_info.hh"
|
||||
#include "imr/utils.hh"
|
||||
@@ -38,7 +39,6 @@
|
||||
|
||||
class abstract_type;
|
||||
class collection_type_impl;
|
||||
class atomic_cell_or_collection;
|
||||
|
||||
using atomic_cell_value_view = data::value_view;
|
||||
using atomic_cell_value_mutable_view = data::value_mutable_view;
|
||||
|
||||
@@ -26,7 +26,10 @@
|
||||
|
||||
namespace auth {
|
||||
|
||||
constexpr std::string_view allow_all_authenticator_name("org.apache.cassandra.auth.AllowAllAuthenticator");
|
||||
const sstring& allow_all_authenticator_name() {
|
||||
static const sstring name = meta::AUTH_PACKAGE_NAME + "AllowAllAuthenticator";
|
||||
return name;
|
||||
}
|
||||
|
||||
// To ensure correct initialization order, we unfortunately need to use a string literal.
|
||||
static const class_registrator<
|
||||
|
||||
@@ -37,7 +37,7 @@ class migration_manager;
|
||||
|
||||
namespace auth {
|
||||
|
||||
extern const std::string_view allow_all_authenticator_name;
|
||||
const sstring& allow_all_authenticator_name();
|
||||
|
||||
class allow_all_authenticator final : public authenticator {
|
||||
public:
|
||||
@@ -52,8 +52,8 @@ public:
|
||||
return make_ready_future<>();
|
||||
}
|
||||
|
||||
virtual std::string_view qualified_java_name() const override {
|
||||
return allow_all_authenticator_name;
|
||||
virtual const sstring& qualified_java_name() const override {
|
||||
return allow_all_authenticator_name();
|
||||
}
|
||||
|
||||
virtual bool require_authentication() const override {
|
||||
|
||||
@@ -26,7 +26,10 @@
|
||||
|
||||
namespace auth {
|
||||
|
||||
constexpr std::string_view allow_all_authorizer_name("org.apache.cassandra.auth.AllowAllAuthorizer");
|
||||
const sstring& allow_all_authorizer_name() {
|
||||
static const sstring name = meta::AUTH_PACKAGE_NAME + "AllowAllAuthorizer";
|
||||
return name;
|
||||
}
|
||||
|
||||
// To ensure correct initialization order, we unfortunately need to use a string literal.
|
||||
static const class_registrator<
|
||||
|
||||
@@ -34,7 +34,7 @@ class migration_manager;
|
||||
|
||||
namespace auth {
|
||||
|
||||
extern const std::string_view allow_all_authorizer_name;
|
||||
const sstring& allow_all_authorizer_name();
|
||||
|
||||
class allow_all_authorizer final : public authorizer {
|
||||
public:
|
||||
@@ -49,8 +49,8 @@ public:
|
||||
return make_ready_future<>();
|
||||
}
|
||||
|
||||
virtual std::string_view qualified_java_name() const override {
|
||||
return allow_all_authorizer_name;
|
||||
virtual const sstring& qualified_java_name() const override {
|
||||
return allow_all_authorizer_name();
|
||||
}
|
||||
|
||||
virtual future<permission_set> authorize(const role_or_anonymous&, const resource&) const override {
|
||||
|
||||
@@ -96,7 +96,7 @@ public:
|
||||
///
|
||||
/// A fully-qualified (class with package) Java-like name for this implementation.
|
||||
///
|
||||
virtual std::string_view qualified_java_name() const = 0;
|
||||
virtual const sstring& qualified_java_name() const = 0;
|
||||
|
||||
virtual bool require_authentication() const = 0;
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ public:
|
||||
///
|
||||
/// A fully-qualified (class with package) Java-like name for this implementation.
|
||||
///
|
||||
virtual std::string_view qualified_java_name() const = 0;
|
||||
virtual const sstring& qualified_java_name() const = 0;
|
||||
|
||||
///
|
||||
/// Query for the permissions granted directly to a role for a particular \ref resource (and not any of its
|
||||
|
||||
@@ -34,9 +34,10 @@ namespace auth {
|
||||
|
||||
namespace meta {
|
||||
|
||||
constexpr std::string_view AUTH_KS("system_auth");
|
||||
constexpr std::string_view USERS_CF("users");
|
||||
constexpr std::string_view AUTH_PACKAGE_NAME("org.apache.cassandra.auth.");
|
||||
const sstring DEFAULT_SUPERUSER_NAME("cassandra");
|
||||
const sstring AUTH_KS("system_auth");
|
||||
const sstring USERS_CF("users");
|
||||
const sstring AUTH_PACKAGE_NAME("org.apache.cassandra.auth.");
|
||||
|
||||
}
|
||||
|
||||
@@ -58,22 +59,22 @@ future<> do_after_system_ready(seastar::abort_source& as, seastar::noncopyable_f
|
||||
}).discard_result();
|
||||
}
|
||||
|
||||
static future<> create_metadata_table_if_missing_impl(
|
||||
future<> create_metadata_table_if_missing(
|
||||
std::string_view table_name,
|
||||
cql3::query_processor& qp,
|
||||
std::string_view cql,
|
||||
::service::migration_manager& mm) {
|
||||
static auto ignore_existing = [] (seastar::noncopyable_function<future<>()> func) {
|
||||
return futurize_invoke(std::move(func)).handle_exception_type([] (exceptions::already_exists_exception& ignored) { });
|
||||
return futurize_apply(std::move(func)).handle_exception_type([] (exceptions::already_exists_exception& ignored) { });
|
||||
};
|
||||
auto& db = qp.db();
|
||||
auto parsed_statement = cql3::query_processor::parse_statement(cql);
|
||||
auto& parsed_cf_statement = static_cast<cql3::statements::raw::cf_statement&>(*parsed_statement);
|
||||
auto parsed_statement = static_pointer_cast<cql3::statements::raw::cf_statement>(
|
||||
cql3::query_processor::parse_statement(cql));
|
||||
|
||||
parsed_cf_statement.prepare_keyspace(meta::AUTH_KS);
|
||||
parsed_statement->prepare_keyspace(meta::AUTH_KS);
|
||||
|
||||
auto statement = static_pointer_cast<cql3::statements::create_table_statement>(
|
||||
parsed_cf_statement.prepare(db, qp.get_cql_stats())->statement);
|
||||
parsed_statement->prepare(db, qp.get_cql_stats())->statement);
|
||||
|
||||
const auto schema = statement->get_cf_meta_data(qp.db());
|
||||
const auto uuid = generate_legacy_id(schema->ks_name(), schema->cf_name());
|
||||
@@ -84,14 +85,7 @@ static future<> create_metadata_table_if_missing_impl(
|
||||
return ignore_existing([&mm, table = std::move(table)] () {
|
||||
return mm.announce_new_column_family(table, false);
|
||||
});
|
||||
}
|
||||
|
||||
future<> create_metadata_table_if_missing(
|
||||
std::string_view table_name,
|
||||
cql3::query_processor& qp,
|
||||
std::string_view cql,
|
||||
::service::migration_manager& mm) noexcept {
|
||||
return futurize_invoke(create_metadata_table_if_missing_impl, table_name, qp, cql, mm);
|
||||
}
|
||||
|
||||
future<> wait_for_schema_agreement(::service::migration_manager& mm, const database& db, seastar::abort_source& as) {
|
||||
@@ -108,17 +102,10 @@ future<> wait_for_schema_agreement(::service::migration_manager& mm, const datab
|
||||
});
|
||||
}
|
||||
|
||||
::service::query_state& internal_distributed_query_state() noexcept {
|
||||
#ifdef DEBUG
|
||||
// Give the much slower debug tests more headroom for completing auth queries.
|
||||
static const auto t = 30s;
|
||||
#else
|
||||
const timeout_config& internal_distributed_timeout_config() noexcept {
|
||||
static const auto t = 5s;
|
||||
#endif
|
||||
static const timeout_config tc{t, t, t, t, t, t, t};
|
||||
static thread_local ::service::client_state cs(::service::client_state::internal_tag{}, tc);
|
||||
static thread_local ::service::query_state qs(cs, empty_service_permit());
|
||||
return qs;
|
||||
return tc;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,15 +27,13 @@
|
||||
#include <seastar/core/future.hh>
|
||||
#include <seastar/core/abort_source.hh>
|
||||
#include <seastar/util/noncopyable_function.hh>
|
||||
#include <seastar/core/seastar.hh>
|
||||
#include <seastar/core/reactor.hh>
|
||||
#include <seastar/core/resource.hh>
|
||||
#include <seastar/core/sstring.hh>
|
||||
#include <seastar/core/smp.hh>
|
||||
|
||||
#include "log.hh"
|
||||
#include "seastarx.hh"
|
||||
#include "utils/exponential_backoff_retry.hh"
|
||||
#include "service/query_state.hh"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
@@ -54,16 +52,16 @@ namespace auth {
|
||||
|
||||
namespace meta {
|
||||
|
||||
constexpr std::string_view DEFAULT_SUPERUSER_NAME("cassandra");
|
||||
extern const std::string_view AUTH_KS;
|
||||
extern const std::string_view USERS_CF;
|
||||
extern const std::string_view AUTH_PACKAGE_NAME;
|
||||
extern const sstring DEFAULT_SUPERUSER_NAME;
|
||||
extern const sstring AUTH_KS;
|
||||
extern const sstring USERS_CF;
|
||||
extern const sstring AUTH_PACKAGE_NAME;
|
||||
|
||||
}
|
||||
|
||||
template <class Task>
|
||||
future<> once_among_shards(Task&& f) {
|
||||
if (this_shard_id() == 0u) {
|
||||
if (engine().cpu_id() == 0u) {
|
||||
return f();
|
||||
}
|
||||
|
||||
@@ -81,13 +79,13 @@ future<> create_metadata_table_if_missing(
|
||||
std::string_view table_name,
|
||||
cql3::query_processor&,
|
||||
std::string_view cql,
|
||||
::service::migration_manager&) noexcept;
|
||||
::service::migration_manager&);
|
||||
|
||||
future<> wait_for_schema_agreement(::service::migration_manager&, const database&, seastar::abort_source&);
|
||||
|
||||
///
|
||||
/// Time-outs for internal, non-local CQL queries.
|
||||
///
|
||||
::service::query_state& internal_distributed_query_state() noexcept;
|
||||
const timeout_config& internal_distributed_timeout_config() noexcept;
|
||||
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ extern "C" {
|
||||
|
||||
#include <boost/algorithm/string/join.hpp>
|
||||
#include <boost/range.hpp>
|
||||
#include <seastar/core/seastar.hh>
|
||||
#include <seastar/core/reactor.hh>
|
||||
|
||||
#include "auth/authenticated_user.hh"
|
||||
#include "auth/common.hh"
|
||||
@@ -65,14 +65,15 @@ extern "C" {
|
||||
|
||||
namespace auth {
|
||||
|
||||
std::string_view default_authorizer::qualified_java_name() const {
|
||||
return "org.apache.cassandra.auth.CassandraAuthorizer";
|
||||
const sstring& default_authorizer_name() {
|
||||
static const sstring name = meta::AUTH_PACKAGE_NAME + "CassandraAuthorizer";
|
||||
return name;
|
||||
}
|
||||
|
||||
static constexpr std::string_view ROLE_NAME = "role";
|
||||
static constexpr std::string_view RESOURCE_NAME = "resource";
|
||||
static constexpr std::string_view PERMISSIONS_NAME = "permissions";
|
||||
static constexpr std::string_view PERMISSIONS_CF = "role_permissions";
|
||||
static const sstring ROLE_NAME = "role";
|
||||
static const sstring RESOURCE_NAME = "resource";
|
||||
static const sstring PERMISSIONS_NAME = "permissions";
|
||||
static const sstring PERMISSIONS_CF = "role_permissions";
|
||||
|
||||
static logging::logger alogger("default_authorizer");
|
||||
|
||||
@@ -100,9 +101,10 @@ bool default_authorizer::legacy_metadata_exists() const {
|
||||
future<bool> default_authorizer::any_granted() const {
|
||||
static const sstring query = format("SELECT * FROM {}.{} LIMIT 1", meta::AUTH_KS, PERMISSIONS_CF);
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query,
|
||||
db::consistency_level::LOCAL_ONE,
|
||||
infinite_timeout_config,
|
||||
{},
|
||||
true).then([this](::shared_ptr<cql3::untyped_result_set> results) {
|
||||
return !results->empty();
|
||||
@@ -113,9 +115,10 @@ future<> default_authorizer::migrate_legacy_metadata() const {
|
||||
alogger.info("Starting migration of legacy permissions metadata.");
|
||||
static const sstring query = format("SELECT * FROM {}.{}", meta::AUTH_KS, legacy_table_name);
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query,
|
||||
db::consistency_level::LOCAL_ONE).then([this](::shared_ptr<cql3::untyped_result_set> results) {
|
||||
db::consistency_level::LOCAL_ONE,
|
||||
infinite_timeout_config).then([this](::shared_ptr<cql3::untyped_result_set> results) {
|
||||
return do_for_each(*results, [this](const cql3::untyped_result_set_row& row) {
|
||||
return do_with(
|
||||
row.get_as<sstring>("username"),
|
||||
@@ -192,9 +195,10 @@ default_authorizer::authorize(const role_or_anonymous& maybe_role, const resourc
|
||||
ROLE_NAME,
|
||||
RESOURCE_NAME);
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query,
|
||||
db::consistency_level::LOCAL_ONE,
|
||||
infinite_timeout_config,
|
||||
{*maybe_role.name, r.name()}).then([](::shared_ptr<cql3::untyped_result_set> results) {
|
||||
if (results->empty()) {
|
||||
return permissions::NONE;
|
||||
@@ -220,10 +224,10 @@ default_authorizer::modify(
|
||||
ROLE_NAME,
|
||||
RESOURCE_NAME),
|
||||
[this, &role_name, set, &resource](const auto& query) {
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query,
|
||||
db::consistency_level::ONE,
|
||||
internal_distributed_query_state(),
|
||||
internal_distributed_timeout_config(),
|
||||
{permissions::to_strings(set), sstring(role_name), resource.name()}).discard_result();
|
||||
});
|
||||
}
|
||||
@@ -245,10 +249,10 @@ future<std::vector<permission_details>> default_authorizer::list_all() const {
|
||||
meta::AUTH_KS,
|
||||
PERMISSIONS_CF);
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query,
|
||||
db::consistency_level::ONE,
|
||||
internal_distributed_query_state(),
|
||||
internal_distributed_timeout_config(),
|
||||
{},
|
||||
true).then([](::shared_ptr<cql3::untyped_result_set> results) {
|
||||
std::vector<permission_details> all_details;
|
||||
@@ -272,10 +276,10 @@ future<> default_authorizer::revoke_all(std::string_view role_name) const {
|
||||
PERMISSIONS_CF,
|
||||
ROLE_NAME);
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query,
|
||||
db::consistency_level::ONE,
|
||||
internal_distributed_query_state(),
|
||||
internal_distributed_timeout_config(),
|
||||
{sstring(role_name)}).discard_result().handle_exception([role_name](auto ep) {
|
||||
try {
|
||||
std::rethrow_exception(ep);
|
||||
@@ -292,9 +296,10 @@ future<> default_authorizer::revoke_all(const resource& resource) const {
|
||||
PERMISSIONS_CF,
|
||||
RESOURCE_NAME);
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query,
|
||||
db::consistency_level::LOCAL_ONE,
|
||||
infinite_timeout_config,
|
||||
{resource.name()}).then_wrapped([this, resource](future<::shared_ptr<cql3::untyped_result_set>> f) {
|
||||
try {
|
||||
auto res = f.get0();
|
||||
@@ -308,9 +313,10 @@ future<> default_authorizer::revoke_all(const resource& resource) const {
|
||||
ROLE_NAME,
|
||||
RESOURCE_NAME);
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query,
|
||||
db::consistency_level::LOCAL_ONE,
|
||||
infinite_timeout_config,
|
||||
{r.get_as<sstring>(ROLE_NAME), resource.name()}).discard_result().handle_exception(
|
||||
[resource](auto ep) {
|
||||
try {
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
|
||||
namespace auth {
|
||||
|
||||
const sstring& default_authorizer_name();
|
||||
|
||||
class default_authorizer : public authorizer {
|
||||
cql3::query_processor& _qp;
|
||||
|
||||
@@ -69,7 +71,9 @@ public:
|
||||
|
||||
virtual future<> stop() override;
|
||||
|
||||
virtual std::string_view qualified_java_name() const override;
|
||||
virtual const sstring& qualified_java_name() const override {
|
||||
return default_authorizer_name();
|
||||
}
|
||||
|
||||
virtual future<permission_set> authorize(const role_or_anonymous&, const resource&) const override;
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
#include <optional>
|
||||
|
||||
#include <boost/algorithm/cxx11/all_of.hpp>
|
||||
#include <seastar/core/seastar.hh>
|
||||
#include <seastar/core/reactor.hh>
|
||||
|
||||
#include "auth/authenticated_user.hh"
|
||||
#include "auth/common.hh"
|
||||
@@ -62,13 +62,15 @@
|
||||
|
||||
namespace auth {
|
||||
|
||||
constexpr std::string_view password_authenticator_name("org.apache.cassandra.auth.PasswordAuthenticator");
|
||||
const sstring& password_authenticator_name() {
|
||||
static const sstring name = meta::AUTH_PACKAGE_NAME + "PasswordAuthenticator";
|
||||
return name;
|
||||
}
|
||||
|
||||
// name of the hash column.
|
||||
static constexpr std::string_view SALTED_HASH = "salted_hash";
|
||||
static constexpr std::string_view OPTIONS = "options";
|
||||
static constexpr std::string_view DEFAULT_USER_NAME = meta::DEFAULT_SUPERUSER_NAME;
|
||||
static const sstring DEFAULT_USER_PASSWORD = sstring(meta::DEFAULT_SUPERUSER_NAME);
|
||||
static const sstring SALTED_HASH = "salted_hash";
|
||||
static const sstring DEFAULT_USER_NAME = meta::DEFAULT_SUPERUSER_NAME;
|
||||
static const sstring DEFAULT_USER_PASSWORD = meta::DEFAULT_SUPERUSER_NAME;
|
||||
|
||||
static logging::logger plogger("password_authenticator");
|
||||
|
||||
@@ -94,13 +96,10 @@ static bool has_salted_hash(const cql3::untyped_result_set_row& row) {
|
||||
return !row.get_or<sstring>(SALTED_HASH, "").empty();
|
||||
}
|
||||
|
||||
static const sstring& update_row_query() {
|
||||
static const sstring update_row_query = format("UPDATE {} SET {} = ? WHERE {} = ?",
|
||||
meta::roles_table::qualified_name,
|
||||
SALTED_HASH,
|
||||
meta::roles_table::role_col_name);
|
||||
return update_row_query;
|
||||
}
|
||||
static const sstring update_row_query = format("UPDATE {} SET {} = ? WHERE {} = ?",
|
||||
meta::roles_table::qualified_name(),
|
||||
SALTED_HASH,
|
||||
meta::roles_table::role_col_name);
|
||||
|
||||
static const sstring legacy_table_name{"credentials"};
|
||||
|
||||
@@ -112,18 +111,18 @@ future<> password_authenticator::migrate_legacy_metadata() const {
|
||||
plogger.info("Starting migration of legacy authentication metadata.");
|
||||
static const sstring query = format("SELECT * FROM {}.{}", meta::AUTH_KS, legacy_table_name);
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query,
|
||||
db::consistency_level::QUORUM,
|
||||
internal_distributed_query_state()).then([this](::shared_ptr<cql3::untyped_result_set> results) {
|
||||
internal_distributed_timeout_config()).then([this](::shared_ptr<cql3::untyped_result_set> results) {
|
||||
return do_for_each(*results, [this](const cql3::untyped_result_set_row& row) {
|
||||
auto username = row.get_as<sstring>("username");
|
||||
auto salted_hash = row.get_as<sstring>(SALTED_HASH);
|
||||
|
||||
return _qp.execute_internal(
|
||||
update_row_query(),
|
||||
return _qp.process(
|
||||
update_row_query,
|
||||
consistency_for_user(username),
|
||||
internal_distributed_query_state(),
|
||||
internal_distributed_timeout_config(),
|
||||
{std::move(salted_hash), username}).discard_result();
|
||||
}).finally([results] {});
|
||||
}).then([] {
|
||||
@@ -137,10 +136,10 @@ future<> password_authenticator::migrate_legacy_metadata() const {
|
||||
future<> password_authenticator::create_default_if_missing() const {
|
||||
return default_role_row_satisfies(_qp, &has_salted_hash).then([this](bool exists) {
|
||||
if (!exists) {
|
||||
return _qp.execute_internal(
|
||||
update_row_query(),
|
||||
return _qp.process(
|
||||
update_row_query,
|
||||
db::consistency_level::QUORUM,
|
||||
internal_distributed_query_state(),
|
||||
internal_distributed_timeout_config(),
|
||||
{passwords::hash(DEFAULT_USER_PASSWORD, rng_for_salt), DEFAULT_USER_NAME}).then([](auto&&) {
|
||||
plogger.info("Created default superuser authentication record.");
|
||||
});
|
||||
@@ -195,8 +194,8 @@ db::consistency_level password_authenticator::consistency_for_user(std::string_v
|
||||
return db::consistency_level::LOCAL_ONE;
|
||||
}
|
||||
|
||||
std::string_view password_authenticator::qualified_java_name() const {
|
||||
return password_authenticator_name;
|
||||
const sstring& password_authenticator::qualified_java_name() const {
|
||||
return password_authenticator_name();
|
||||
}
|
||||
|
||||
bool password_authenticator::require_authentication() const {
|
||||
@@ -204,19 +203,19 @@ bool password_authenticator::require_authentication() const {
|
||||
}
|
||||
|
||||
authentication_option_set password_authenticator::supported_options() const {
|
||||
return authentication_option_set{authentication_option::password, authentication_option::options};
|
||||
return authentication_option_set{authentication_option::password};
|
||||
}
|
||||
|
||||
authentication_option_set password_authenticator::alterable_options() const {
|
||||
return authentication_option_set{authentication_option::password, authentication_option::options};
|
||||
return authentication_option_set{authentication_option::password};
|
||||
}
|
||||
|
||||
future<authenticated_user> password_authenticator::authenticate(
|
||||
const credentials_map& credentials) const {
|
||||
if (!credentials.contains(USERNAME_KEY)) {
|
||||
if (!credentials.count(USERNAME_KEY)) {
|
||||
throw exceptions::authentication_exception(format("Required key '{}' is missing", USERNAME_KEY));
|
||||
}
|
||||
if (!credentials.contains(PASSWORD_KEY)) {
|
||||
if (!credentials.count(PASSWORD_KEY)) {
|
||||
throw exceptions::authentication_exception(format("Required key '{}' is missing", PASSWORD_KEY));
|
||||
}
|
||||
|
||||
@@ -228,16 +227,16 @@ future<authenticated_user> password_authenticator::authenticate(
|
||||
// obsolete prepared statements pretty quickly.
|
||||
// Rely on query processing caching statements instead, and lets assume
|
||||
// that a map lookup string->statement is not gonna kill us much.
|
||||
return futurize_invoke([this, username, password] {
|
||||
return futurize_apply([this, username, password] {
|
||||
static const sstring query = format("SELECT {} FROM {} WHERE {} = ?",
|
||||
SALTED_HASH,
|
||||
meta::roles_table::qualified_name,
|
||||
meta::roles_table::qualified_name(),
|
||||
meta::roles_table::role_col_name);
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query,
|
||||
consistency_for_user(username),
|
||||
internal_distributed_query_state(),
|
||||
internal_distributed_timeout_config(),
|
||||
{username},
|
||||
true);
|
||||
}).then_wrapped([=](future<::shared_ptr<cql3::untyped_result_set>> f) {
|
||||
@@ -263,91 +262,49 @@ future<authenticated_user> password_authenticator::authenticate(
|
||||
});
|
||||
}
|
||||
|
||||
future<> password_authenticator::maybe_update_custom_options(std::string_view role_name, const authentication_options& options) const {
|
||||
static const sstring query = format("UPDATE {} SET {} = ? WHERE {} = ?",
|
||||
meta::roles_table::qualified_name,
|
||||
OPTIONS,
|
||||
meta::roles_table::role_col_name);
|
||||
|
||||
if (!options.options) {
|
||||
future<> password_authenticator::create(std::string_view role_name, const authentication_options& options) const {
|
||||
if (!options.password) {
|
||||
return make_ready_future<>();
|
||||
}
|
||||
|
||||
std::vector<std::pair<data_value, data_value>> entries;
|
||||
for (const auto& entry : *options.options) {
|
||||
entries.push_back({data_value(entry.first), data_value(entry.second)});
|
||||
}
|
||||
auto map_value = make_map_value(map_type_impl::get_instance(utf8_type, utf8_type, false), entries);
|
||||
|
||||
return _qp.execute_internal(
|
||||
query,
|
||||
return _qp.process(
|
||||
update_row_query,
|
||||
consistency_for_user(role_name),
|
||||
internal_distributed_query_state(),
|
||||
{std::move(map_value), sstring(role_name)}).discard_result();
|
||||
}
|
||||
|
||||
future<> password_authenticator::create(std::string_view role_name, const authentication_options& options) const {
|
||||
if (!options.password) {
|
||||
return maybe_update_custom_options(role_name, options);
|
||||
}
|
||||
|
||||
return _qp.execute_internal(
|
||||
update_row_query(),
|
||||
consistency_for_user(role_name),
|
||||
internal_distributed_query_state(),
|
||||
{passwords::hash(*options.password, rng_for_salt), sstring(role_name)}).discard_result().then([this, role_name, &options] {
|
||||
return maybe_update_custom_options(role_name, options);
|
||||
});
|
||||
internal_distributed_timeout_config(),
|
||||
{passwords::hash(*options.password, rng_for_salt), sstring(role_name)}).discard_result();
|
||||
}
|
||||
|
||||
future<> password_authenticator::alter(std::string_view role_name, const authentication_options& options) const {
|
||||
if (!options.password) {
|
||||
return maybe_update_custom_options(role_name, options);
|
||||
return make_ready_future<>();
|
||||
}
|
||||
|
||||
static const sstring query = format("UPDATE {} SET {} = ? WHERE {} = ?",
|
||||
meta::roles_table::qualified_name,
|
||||
meta::roles_table::qualified_name(),
|
||||
SALTED_HASH,
|
||||
meta::roles_table::role_col_name);
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query,
|
||||
consistency_for_user(role_name),
|
||||
internal_distributed_query_state(),
|
||||
{passwords::hash(*options.password, rng_for_salt), sstring(role_name)}).discard_result().then([this, role_name, &options] {
|
||||
return maybe_update_custom_options(role_name, options);
|
||||
}).discard_result();
|
||||
internal_distributed_timeout_config(),
|
||||
{passwords::hash(*options.password, rng_for_salt), sstring(role_name)}).discard_result();
|
||||
}
|
||||
|
||||
future<> password_authenticator::drop(std::string_view name) const {
|
||||
static const sstring query = format("DELETE {} FROM {} WHERE {} = ?",
|
||||
SALTED_HASH,
|
||||
meta::roles_table::qualified_name,
|
||||
meta::roles_table::qualified_name(),
|
||||
meta::roles_table::role_col_name);
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query, consistency_for_user(name),
|
||||
internal_distributed_query_state(),
|
||||
internal_distributed_timeout_config(),
|
||||
{sstring(name)}).discard_result();
|
||||
}
|
||||
|
||||
future<custom_options> password_authenticator::query_custom_options(std::string_view role_name) const {
|
||||
static const sstring query = format("SELECT {} FROM {} WHERE {} = ?",
|
||||
OPTIONS,
|
||||
meta::roles_table::qualified_name,
|
||||
meta::roles_table::role_col_name);
|
||||
|
||||
return _qp.execute_internal(
|
||||
query, consistency_for_user(role_name),
|
||||
internal_distributed_query_state(),
|
||||
{sstring(role_name)}).then([](::shared_ptr<cql3::untyped_result_set> rs) {
|
||||
custom_options opts;
|
||||
const auto& row = rs->one();
|
||||
if (row.has(OPTIONS)) {
|
||||
row.get_map_data<sstring, sstring>(OPTIONS, std::inserter(opts, opts.end()), utf8_type, utf8_type);
|
||||
}
|
||||
return opts;
|
||||
});
|
||||
return make_ready_future<custom_options>();
|
||||
}
|
||||
|
||||
const resource_set& password_authenticator::protected_resources() const {
|
||||
|
||||
@@ -52,7 +52,7 @@ class migration_manager;
|
||||
|
||||
namespace auth {
|
||||
|
||||
extern const std::string_view password_authenticator_name;
|
||||
const sstring& password_authenticator_name();
|
||||
|
||||
class password_authenticator : public authenticator {
|
||||
cql3::query_processor& _qp;
|
||||
@@ -71,7 +71,7 @@ public:
|
||||
|
||||
virtual future<> stop() override;
|
||||
|
||||
virtual std::string_view qualified_java_name() const override;
|
||||
virtual const sstring& qualified_java_name() const override;
|
||||
|
||||
virtual bool require_authentication() const override;
|
||||
|
||||
@@ -94,8 +94,6 @@ public:
|
||||
virtual ::shared_ptr<sasl_challenge> new_sasl_challenge() const override;
|
||||
|
||||
private:
|
||||
future<> maybe_update_custom_options(std::string_view role_name, const authentication_options& options) const;
|
||||
|
||||
bool legacy_metadata_exists() const;
|
||||
|
||||
future<> migrate_legacy_metadata() const;
|
||||
|
||||
@@ -43,16 +43,18 @@ std::string_view creation_query() {
|
||||
" can_login boolean,"
|
||||
" is_superuser boolean,"
|
||||
" member_of set<text>,"
|
||||
" salted_hash text,"
|
||||
" options frozen<map<text, text>>,"
|
||||
" salted_hash text"
|
||||
")",
|
||||
qualified_name,
|
||||
qualified_name(),
|
||||
role_col_name);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
constexpr std::string_view qualified_name("system_auth.roles");
|
||||
std::string_view qualified_name() noexcept {
|
||||
static const sstring instance = AUTH_KS + "." + sstring(name);
|
||||
return instance;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -62,20 +64,21 @@ future<bool> default_role_row_satisfies(
|
||||
cql3::query_processor& qp,
|
||||
std::function<bool(const cql3::untyped_result_set_row&)> p) {
|
||||
static const sstring query = format("SELECT * FROM {} WHERE {} = ?",
|
||||
meta::roles_table::qualified_name,
|
||||
meta::roles_table::qualified_name(),
|
||||
meta::roles_table::role_col_name);
|
||||
|
||||
return do_with(std::move(p), [&qp](const auto& p) {
|
||||
return qp.execute_internal(
|
||||
return qp.process(
|
||||
query,
|
||||
db::consistency_level::ONE,
|
||||
infinite_timeout_config,
|
||||
{meta::DEFAULT_SUPERUSER_NAME},
|
||||
true).then([&qp, &p](::shared_ptr<cql3::untyped_result_set> results) {
|
||||
if (results->empty()) {
|
||||
return qp.execute_internal(
|
||||
return qp.process(
|
||||
query,
|
||||
db::consistency_level::QUORUM,
|
||||
internal_distributed_query_state(),
|
||||
internal_distributed_timeout_config(),
|
||||
{meta::DEFAULT_SUPERUSER_NAME},
|
||||
true).then([&p](::shared_ptr<cql3::untyped_result_set> results) {
|
||||
if (results->empty()) {
|
||||
@@ -94,13 +97,13 @@ future<bool> default_role_row_satisfies(
|
||||
future<bool> any_nondefault_role_row_satisfies(
|
||||
cql3::query_processor& qp,
|
||||
std::function<bool(const cql3::untyped_result_set_row&)> p) {
|
||||
static const sstring query = format("SELECT * FROM {}", meta::roles_table::qualified_name);
|
||||
static const sstring query = format("SELECT * FROM {}", meta::roles_table::qualified_name());
|
||||
|
||||
return do_with(std::move(p), [&qp](const auto& p) {
|
||||
return qp.execute_internal(
|
||||
return qp.process(
|
||||
query,
|
||||
db::consistency_level::QUORUM,
|
||||
internal_distributed_query_state()).then([&p](::shared_ptr<cql3::untyped_result_set> results) {
|
||||
internal_distributed_timeout_config()).then([&p](::shared_ptr<cql3::untyped_result_set> results) {
|
||||
if (results->empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ std::string_view creation_query();
|
||||
|
||||
constexpr std::string_view name{"roles", 5};
|
||||
|
||||
extern const std::string_view qualified_name;
|
||||
std::string_view qualified_name() noexcept;
|
||||
|
||||
constexpr std::string_view role_col_name{"role", 4};
|
||||
|
||||
|
||||
@@ -31,7 +31,9 @@
|
||||
#include "auth/allow_all_authenticator.hh"
|
||||
#include "auth/allow_all_authorizer.hh"
|
||||
#include "auth/common.hh"
|
||||
#include "auth/password_authenticator.hh"
|
||||
#include "auth/role_or_anonymous.hh"
|
||||
#include "auth/standard_role_manager.hh"
|
||||
#include "cql3/query_processor.hh"
|
||||
#include "cql3/untyped_result_set.hh"
|
||||
#include "db/consistency_level_type.hh"
|
||||
@@ -123,7 +125,18 @@ service::service(
|
||||
, _authorizer(std::move(z))
|
||||
, _authenticator(std::move(a))
|
||||
, _role_manager(std::move(r))
|
||||
, _migration_listener(std::make_unique<auth_migration_listener>(*_authorizer)) {}
|
||||
, _migration_listener(std::make_unique<auth_migration_listener>(*_authorizer)) {
|
||||
// The password authenticator requires that the `standard_role_manager` is running so that the roles metadata table
|
||||
// it manages is created and updated. This cross-module dependency is rather gross, but we have to maintain it for
|
||||
// the sake of compatibility with Apache Cassandra and its choice of auth. schema.
|
||||
if ((_authenticator->qualified_java_name() == password_authenticator_name())
|
||||
&& (_role_manager->qualified_java_name() != standard_role_manager_name())) {
|
||||
throw incompatible_module_combination(
|
||||
format("The {} authenticator must be loaded alongside the {} role-manager.",
|
||||
password_authenticator_name(),
|
||||
standard_role_manager_name()));
|
||||
}
|
||||
}
|
||||
|
||||
service::service(
|
||||
permissions_cache_config c,
|
||||
@@ -165,7 +178,7 @@ future<> service::start(::service::migration_manager& mm) {
|
||||
return create_keyspace_if_missing(mm);
|
||||
}).then([this] {
|
||||
return _role_manager->start().then([this] {
|
||||
return when_all_succeed(_authorizer->start(), _authenticator->start()).discard_result();
|
||||
return when_all_succeed(_authorizer->start(), _authenticator->start());
|
||||
});
|
||||
}).then([this] {
|
||||
_permissions_cache = std::make_unique<permissions_cache>(_permissions_cache_config, *this, log);
|
||||
@@ -181,12 +194,9 @@ future<> service::stop() {
|
||||
// Only one of the shards has the listener registered, but let's try to
|
||||
// unregister on each one just to make sure.
|
||||
return _mnotifier.unregister_listener(_migration_listener.get()).then([this] {
|
||||
if (_permissions_cache) {
|
||||
return _permissions_cache->stop();
|
||||
}
|
||||
return make_ready_future<>();
|
||||
return _permissions_cache->stop();
|
||||
}).then([this] {
|
||||
return when_all_succeed(_role_manager->stop(), _authorizer->stop(), _authenticator->stop()).discard_result();
|
||||
return when_all_succeed(_role_manager->stop(), _authorizer->stop(), _authenticator->stop());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -207,27 +217,30 @@ future<bool> service::has_existing_legacy_users() const {
|
||||
// This logic is borrowed directly from Apache Cassandra. By first checking for the presence of the default user, we
|
||||
// can potentially avoid doing a range query with a high consistency level.
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
default_user_query,
|
||||
db::consistency_level::ONE,
|
||||
infinite_timeout_config,
|
||||
{meta::DEFAULT_SUPERUSER_NAME},
|
||||
true).then([this](auto results) {
|
||||
if (!results->empty()) {
|
||||
return make_ready_future<bool>(true);
|
||||
}
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
default_user_query,
|
||||
db::consistency_level::QUORUM,
|
||||
infinite_timeout_config,
|
||||
{meta::DEFAULT_SUPERUSER_NAME},
|
||||
true).then([this](auto results) {
|
||||
if (!results->empty()) {
|
||||
return make_ready_future<bool>(true);
|
||||
}
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
all_users_query,
|
||||
db::consistency_level::QUORUM).then([](auto results) {
|
||||
db::consistency_level::QUORUM,
|
||||
infinite_timeout_config).then([](auto results) {
|
||||
return make_ready_future<bool>(!results->empty());
|
||||
});
|
||||
});
|
||||
@@ -360,28 +373,25 @@ future<permission_set> get_permissions(const service& ser, const authenticated_u
|
||||
}
|
||||
|
||||
bool is_enforcing(const service& ser) {
|
||||
const bool enforcing_authorizer = ser.underlying_authorizer().qualified_java_name() != allow_all_authorizer_name;
|
||||
const bool enforcing_authorizer = ser.underlying_authorizer().qualified_java_name() != allow_all_authorizer_name();
|
||||
|
||||
const bool enforcing_authenticator = ser.underlying_authenticator().qualified_java_name()
|
||||
!= allow_all_authenticator_name;
|
||||
!= allow_all_authenticator_name();
|
||||
|
||||
return enforcing_authorizer || enforcing_authenticator;
|
||||
}
|
||||
|
||||
bool is_protected(const service& ser, command_desc cmd) noexcept {
|
||||
if (cmd.type_ == command_desc::type::ALTER_WITH_OPTS) {
|
||||
return false; // Table attributes are OK to modify; see #7057.
|
||||
}
|
||||
return ser.underlying_role_manager().protected_resources().contains(cmd.resource)
|
||||
|| ser.underlying_authenticator().protected_resources().contains(cmd.resource)
|
||||
|| ser.underlying_authorizer().protected_resources().contains(cmd.resource);
|
||||
bool is_protected(const service& ser, const resource& r) noexcept {
|
||||
return ser.underlying_role_manager().protected_resources().count(r)
|
||||
|| ser.underlying_authenticator().protected_resources().count(r)
|
||||
|| ser.underlying_authorizer().protected_resources().count(r);
|
||||
}
|
||||
|
||||
static void validate_authentication_options_are_supported(
|
||||
const authentication_options& options,
|
||||
const authentication_option_set& supported) {
|
||||
const auto check = [&supported](authentication_option k) {
|
||||
if (!supported.contains(k)) {
|
||||
if (supported.count(k) == 0) {
|
||||
throw unsupported_authentication_option(k);
|
||||
}
|
||||
};
|
||||
@@ -406,7 +416,7 @@ future<> create_role(
|
||||
return make_ready_future<>();
|
||||
}
|
||||
|
||||
return futurize_invoke(
|
||||
return futurize_apply(
|
||||
&validate_authentication_options_are_supported,
|
||||
options,
|
||||
ser.underlying_authenticator().supported_options()).then([&ser, name, &options] {
|
||||
@@ -430,7 +440,7 @@ future<> alter_role(
|
||||
return make_ready_future<>();
|
||||
}
|
||||
|
||||
return futurize_invoke(
|
||||
return futurize_apply(
|
||||
&validate_authentication_options_are_supported,
|
||||
options,
|
||||
ser.underlying_authenticator().supported_options()).then([&ser, name, &options] {
|
||||
@@ -445,9 +455,7 @@ future<> drop_role(const service& ser, std::string_view name) {
|
||||
|
||||
return when_all_succeed(
|
||||
a.revoke_all(name),
|
||||
a.revoke_all(r))
|
||||
.discard_result()
|
||||
.handle_exception_type([](const unsupported_authorization_operation&) {
|
||||
a.revoke_all(r)).handle_exception_type([](const unsupported_authorization_operation&) {
|
||||
// Nothing.
|
||||
});
|
||||
}).then([&ser, name] {
|
||||
@@ -460,8 +468,8 @@ future<> drop_role(const service& ser, std::string_view name) {
|
||||
future<bool> has_role(const service& ser, std::string_view grantee, std::string_view name) {
|
||||
return when_all_succeed(
|
||||
validate_role_exists(ser, name),
|
||||
ser.get_roles(grantee)).then_unpack([name](role_set all_roles) {
|
||||
return make_ready_future<bool>(all_roles.contains(sstring(name)));
|
||||
ser.get_roles(grantee)).then([name](role_set all_roles) {
|
||||
return make_ready_future<bool>(all_roles.count(sstring(name)) != 0);
|
||||
});
|
||||
}
|
||||
future<bool> has_role(const service& ser, const authenticated_user& u, std::string_view name) {
|
||||
@@ -518,9 +526,14 @@ future<std::vector<permission_details>> list_filtered_permissions(
|
||||
? auth::expand_resource_family(r)
|
||||
: auth::resource_set{r};
|
||||
|
||||
std::erase_if(all_details, [&resources](const permission_details& pd) {
|
||||
return !resources.contains(pd.resource);
|
||||
});
|
||||
all_details.erase(
|
||||
std::remove_if(
|
||||
all_details.begin(),
|
||||
all_details.end(),
|
||||
[&resources](const permission_details& pd) {
|
||||
return resources.count(pd.resource) == 0;
|
||||
}),
|
||||
all_details.end());
|
||||
}
|
||||
|
||||
std::transform(
|
||||
@@ -533,9 +546,11 @@ future<std::vector<permission_details>> list_filtered_permissions(
|
||||
});
|
||||
|
||||
// Eliminate rows with an empty permission set.
|
||||
std::erase_if(all_details, [](const permission_details& pd) {
|
||||
return pd.permissions.mask() == 0;
|
||||
});
|
||||
all_details.erase(
|
||||
std::remove_if(all_details.begin(), all_details.end(), [](const permission_details& pd) {
|
||||
return pd.permissions.mask() == 0;
|
||||
}),
|
||||
all_details.end());
|
||||
|
||||
if (!role_name) {
|
||||
return make_ready_future<std::vector<permission_details>>(std::move(all_details));
|
||||
@@ -547,9 +562,14 @@ future<std::vector<permission_details>> list_filtered_permissions(
|
||||
|
||||
return do_with(std::move(all_details), [&ser, role_name](auto& all_details) {
|
||||
return ser.get_roles(*role_name).then([&all_details](role_set all_roles) {
|
||||
std::erase_if(all_details, [&all_roles](const permission_details& pd) {
|
||||
return !all_roles.contains(pd.role_name);
|
||||
});
|
||||
all_details.erase(
|
||||
std::remove_if(
|
||||
all_details.begin(),
|
||||
all_details.end(),
|
||||
[&all_roles](const permission_details& pd) {
|
||||
return all_roles.count(pd.role_name) == 0;
|
||||
}),
|
||||
all_details.end());
|
||||
|
||||
return make_ready_future<std::vector<permission_details>>(std::move(all_details));
|
||||
});
|
||||
|
||||
@@ -181,21 +181,10 @@ future<permission_set> get_permissions(const service&, const authenticated_user&
|
||||
///
|
||||
bool is_enforcing(const service&);
|
||||
|
||||
/// A description of a CQL command from which auth::service can tell whether or not this command could endanger
|
||||
/// internal data on which auth::service depends.
|
||||
struct command_desc {
|
||||
auth::permission permission; ///< Nature of the command's alteration.
|
||||
const ::auth::resource& resource; ///< Resource impacted by this command.
|
||||
enum class type {
|
||||
ALTER_WITH_OPTS, ///< Command is ALTER ... WITH ...
|
||||
OTHER
|
||||
} type_ = type::OTHER;
|
||||
};
|
||||
|
||||
///
|
||||
/// Protected resources cannot be modified even if the performer has permissions to do so.
|
||||
///
|
||||
bool is_protected(const service&, command_desc) noexcept;
|
||||
bool is_protected(const service&, const resource&) noexcept;
|
||||
|
||||
///
|
||||
/// Create a role with optional authentication information.
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
#include "auth/common.hh"
|
||||
#include "auth/roles-metadata.hh"
|
||||
#include "cql3/query_processor.hh"
|
||||
#include "cql3/untyped_result_set.hh"
|
||||
#include "db/consistency_level_type.hh"
|
||||
#include "exceptions/exceptions.hh"
|
||||
#include "log.hh"
|
||||
@@ -49,7 +48,11 @@ namespace meta {
|
||||
namespace role_members_table {
|
||||
|
||||
constexpr std::string_view name{"role_members" , 12};
|
||||
constexpr std::string_view qualified_name("system_auth.role_members");
|
||||
|
||||
static std::string_view qualified_name() noexcept {
|
||||
static const sstring instance = AUTH_KS + "." + sstring(name);
|
||||
return instance;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -80,13 +83,13 @@ static db::consistency_level consistency_for_role(std::string_view role_name) no
|
||||
|
||||
static future<std::optional<record>> find_record(cql3::query_processor& qp, std::string_view role_name) {
|
||||
static const sstring query = format("SELECT * FROM {} WHERE {} = ?",
|
||||
meta::roles_table::qualified_name,
|
||||
meta::roles_table::qualified_name(),
|
||||
meta::roles_table::role_col_name);
|
||||
|
||||
return qp.execute_internal(
|
||||
return qp.process(
|
||||
query,
|
||||
consistency_for_role(role_name),
|
||||
internal_distributed_query_state(),
|
||||
internal_distributed_timeout_config(),
|
||||
{sstring(role_name)},
|
||||
true).then([](::shared_ptr<cql3::untyped_result_set> results) {
|
||||
if (results->empty()) {
|
||||
@@ -120,8 +123,13 @@ static bool has_can_login(const cql3::untyped_result_set_row& row) {
|
||||
return row.has("can_login") && !(boolean_type->deserialize(row.get_blob("can_login")).is_null());
|
||||
}
|
||||
|
||||
std::string_view standard_role_manager_name() noexcept {
|
||||
static const sstring instance = meta::AUTH_PACKAGE_NAME + "CassandraRoleManager";
|
||||
return instance;
|
||||
}
|
||||
|
||||
std::string_view standard_role_manager::qualified_java_name() const noexcept {
|
||||
return "org.apache.cassandra.auth.CassandraRoleManager";
|
||||
return standard_role_manager_name();
|
||||
}
|
||||
|
||||
const resource_set& standard_role_manager::protected_resources() const {
|
||||
@@ -139,7 +147,7 @@ future<> standard_role_manager::create_metadata_tables_if_missing() const {
|
||||
" member text,"
|
||||
" PRIMARY KEY (role, member)"
|
||||
")",
|
||||
meta::role_members_table::qualified_name);
|
||||
meta::role_members_table::qualified_name());
|
||||
|
||||
|
||||
return when_all_succeed(
|
||||
@@ -152,20 +160,20 @@ future<> standard_role_manager::create_metadata_tables_if_missing() const {
|
||||
meta::role_members_table::name,
|
||||
_qp,
|
||||
create_role_members_query,
|
||||
_migration_manager)).discard_result();
|
||||
_migration_manager));
|
||||
}
|
||||
|
||||
future<> standard_role_manager::create_default_role_if_missing() const {
|
||||
return default_role_row_satisfies(_qp, &has_can_login).then([this](bool exists) {
|
||||
if (!exists) {
|
||||
static const sstring query = format("INSERT INTO {} ({}, is_superuser, can_login) VALUES (?, true, true)",
|
||||
meta::roles_table::qualified_name,
|
||||
meta::roles_table::qualified_name(),
|
||||
meta::roles_table::role_col_name);
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query,
|
||||
db::consistency_level::QUORUM,
|
||||
internal_distributed_query_state(),
|
||||
internal_distributed_timeout_config(),
|
||||
{meta::DEFAULT_SUPERUSER_NAME}).then([](auto&&) {
|
||||
log.info("Created default superuser role '{}'.", meta::DEFAULT_SUPERUSER_NAME);
|
||||
return make_ready_future<>();
|
||||
@@ -189,10 +197,10 @@ future<> standard_role_manager::migrate_legacy_metadata() const {
|
||||
log.info("Starting migration of legacy user metadata.");
|
||||
static const sstring query = format("SELECT * FROM {}.{}", meta::AUTH_KS, legacy_table_name);
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query,
|
||||
db::consistency_level::QUORUM,
|
||||
internal_distributed_query_state()).then([this](::shared_ptr<cql3::untyped_result_set> results) {
|
||||
internal_distributed_timeout_config()).then([this](::shared_ptr<cql3::untyped_result_set> results) {
|
||||
return do_for_each(*results, [this](const cql3::untyped_result_set_row& row) {
|
||||
role_config config;
|
||||
config.is_superuser = row.get_or<bool>("super", false);
|
||||
@@ -247,13 +255,13 @@ future<> standard_role_manager::stop() {
|
||||
|
||||
future<> standard_role_manager::create_or_replace(std::string_view role_name, const role_config& c) const {
|
||||
static const sstring query = format("INSERT INTO {} ({}, is_superuser, can_login) VALUES (?, ?, ?)",
|
||||
meta::roles_table::qualified_name,
|
||||
meta::roles_table::qualified_name(),
|
||||
meta::roles_table::role_col_name);
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query,
|
||||
consistency_for_role(role_name),
|
||||
internal_distributed_query_state(),
|
||||
internal_distributed_timeout_config(),
|
||||
{sstring(role_name), c.is_superuser, c.can_login},
|
||||
true).discard_result();
|
||||
}
|
||||
@@ -290,13 +298,13 @@ standard_role_manager::alter(std::string_view role_name, const role_config_updat
|
||||
return make_ready_future<>();
|
||||
}
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
format("UPDATE {} SET {} WHERE {} = ?",
|
||||
meta::roles_table::qualified_name,
|
||||
meta::roles_table::qualified_name(),
|
||||
build_column_assignments(u),
|
||||
meta::roles_table::role_col_name),
|
||||
consistency_for_role(role_name),
|
||||
internal_distributed_query_state(),
|
||||
internal_distributed_timeout_config(),
|
||||
{sstring(role_name)}).discard_result();
|
||||
});
|
||||
}
|
||||
@@ -310,12 +318,12 @@ future<> standard_role_manager::drop(std::string_view role_name) const {
|
||||
// First, revoke this role from all roles that are members of it.
|
||||
const auto revoke_from_members = [this, role_name] {
|
||||
static const sstring query = format("SELECT member FROM {} WHERE role = ?",
|
||||
meta::role_members_table::qualified_name);
|
||||
meta::role_members_table::qualified_name());
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query,
|
||||
consistency_for_role(role_name),
|
||||
internal_distributed_query_state(),
|
||||
internal_distributed_timeout_config(),
|
||||
{sstring(role_name)}).then([this, role_name](::shared_ptr<cql3::untyped_result_set> members) {
|
||||
return parallel_for_each(
|
||||
members->begin(),
|
||||
@@ -348,17 +356,17 @@ future<> standard_role_manager::drop(std::string_view role_name) const {
|
||||
// Finally, delete the role itself.
|
||||
auto delete_role = [this, role_name] {
|
||||
static const sstring query = format("DELETE FROM {} WHERE {} = ?",
|
||||
meta::roles_table::qualified_name,
|
||||
meta::roles_table::qualified_name(),
|
||||
meta::roles_table::role_col_name);
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query,
|
||||
consistency_for_role(role_name),
|
||||
internal_distributed_query_state(),
|
||||
internal_distributed_timeout_config(),
|
||||
{sstring(role_name)}).discard_result();
|
||||
};
|
||||
|
||||
return when_all_succeed(revoke_from_members(), revoke_members_of()).then_unpack([delete_role = std::move(delete_role)] {
|
||||
return when_all_succeed(revoke_from_members(), revoke_members_of()).then([delete_role = std::move(delete_role)] {
|
||||
return delete_role();
|
||||
});
|
||||
});
|
||||
@@ -374,40 +382,40 @@ standard_role_manager::modify_membership(
|
||||
const auto modify_roles = [this, role_name, grantee_name, ch] {
|
||||
const auto query = format(
|
||||
"UPDATE {} SET member_of = member_of {} ? WHERE {} = ?",
|
||||
meta::roles_table::qualified_name,
|
||||
meta::roles_table::qualified_name(),
|
||||
(ch == membership_change::add ? '+' : '-'),
|
||||
meta::roles_table::role_col_name);
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query,
|
||||
consistency_for_role(grantee_name),
|
||||
internal_distributed_query_state(),
|
||||
internal_distributed_timeout_config(),
|
||||
{role_set{sstring(role_name)}, sstring(grantee_name)}).discard_result();
|
||||
};
|
||||
|
||||
const auto modify_role_members = [this, role_name, grantee_name, ch] {
|
||||
switch (ch) {
|
||||
case membership_change::add:
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
format("INSERT INTO {} (role, member) VALUES (?, ?)",
|
||||
meta::role_members_table::qualified_name),
|
||||
meta::role_members_table::qualified_name()),
|
||||
consistency_for_role(role_name),
|
||||
internal_distributed_query_state(),
|
||||
internal_distributed_timeout_config(),
|
||||
{sstring(role_name), sstring(grantee_name)}).discard_result();
|
||||
|
||||
case membership_change::remove:
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
format("DELETE FROM {} WHERE role = ? AND member = ?",
|
||||
meta::role_members_table::qualified_name),
|
||||
meta::role_members_table::qualified_name()),
|
||||
consistency_for_role(role_name),
|
||||
internal_distributed_query_state(),
|
||||
internal_distributed_timeout_config(),
|
||||
{sstring(role_name), sstring(grantee_name)}).discard_result();
|
||||
}
|
||||
|
||||
return make_ready_future<>();
|
||||
};
|
||||
|
||||
return when_all_succeed(modify_roles(), modify_role_members).discard_result();
|
||||
return when_all_succeed(modify_roles(), modify_role_members());
|
||||
}
|
||||
|
||||
future<>
|
||||
@@ -416,7 +424,7 @@ standard_role_manager::grant(std::string_view grantee_name, std::string_view rol
|
||||
return this->query_granted(
|
||||
grantee_name,
|
||||
recursive_role_query::yes).then([role_name, grantee_name](role_set roles) {
|
||||
if (roles.contains(sstring(role_name))) {
|
||||
if (roles.count(sstring(role_name)) != 0) {
|
||||
throw role_already_included(grantee_name, role_name);
|
||||
}
|
||||
|
||||
@@ -428,7 +436,7 @@ standard_role_manager::grant(std::string_view grantee_name, std::string_view rol
|
||||
return this->query_granted(
|
||||
role_name,
|
||||
recursive_role_query::yes).then([role_name, grantee_name](role_set roles) {
|
||||
if (roles.contains(sstring(grantee_name))) {
|
||||
if (roles.count(sstring(grantee_name)) != 0) {
|
||||
throw role_already_included(role_name, grantee_name);
|
||||
}
|
||||
|
||||
@@ -436,7 +444,7 @@ standard_role_manager::grant(std::string_view grantee_name, std::string_view rol
|
||||
});
|
||||
};
|
||||
|
||||
return when_all_succeed(check_redundant(), check_cycle()).then_unpack([this, role_name, grantee_name] {
|
||||
return when_all_succeed(check_redundant(), check_cycle()).then([this, role_name, grantee_name] {
|
||||
return this->modify_membership(grantee_name, role_name, membership_change::add);
|
||||
});
|
||||
}
|
||||
@@ -451,7 +459,7 @@ standard_role_manager::revoke(std::string_view revokee_name, std::string_view ro
|
||||
return this->query_granted(
|
||||
revokee_name,
|
||||
recursive_role_query::no).then([revokee_name, role_name](role_set roles) {
|
||||
if (!roles.contains(sstring(role_name))) {
|
||||
if (roles.count(sstring(role_name)) == 0) {
|
||||
throw revoke_ungranted_role(revokee_name, role_name);
|
||||
}
|
||||
|
||||
@@ -495,15 +503,15 @@ future<role_set> standard_role_manager::query_granted(std::string_view grantee_n
|
||||
future<role_set> standard_role_manager::query_all() const {
|
||||
static const sstring query = format("SELECT {} FROM {}",
|
||||
meta::roles_table::role_col_name,
|
||||
meta::roles_table::qualified_name);
|
||||
meta::roles_table::qualified_name());
|
||||
|
||||
// To avoid many copies of a view.
|
||||
static const auto role_col_name_string = sstring(meta::roles_table::role_col_name);
|
||||
|
||||
return _qp.execute_internal(
|
||||
return _qp.process(
|
||||
query,
|
||||
db::consistency_level::QUORUM,
|
||||
internal_distributed_query_state()).then([](::shared_ptr<cql3::untyped_result_set> results) {
|
||||
internal_distributed_timeout_config()).then([](::shared_ptr<cql3::untyped_result_set> results) {
|
||||
role_set roles;
|
||||
|
||||
std::transform(
|
||||
|
||||
@@ -42,6 +42,8 @@ class migration_manager;
|
||||
|
||||
namespace auth {
|
||||
|
||||
std::string_view standard_role_manager_name() noexcept;
|
||||
|
||||
class standard_role_manager final : public role_manager {
|
||||
cql3::query_processor& _qp;
|
||||
::service::migration_manager& _migration_manager;
|
||||
|
||||
@@ -82,7 +82,7 @@ public:
|
||||
return _authenticator->stop();
|
||||
}
|
||||
|
||||
virtual std::string_view qualified_java_name() const override {
|
||||
virtual const sstring& qualified_java_name() const override {
|
||||
return transitional_authenticator_name();
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ public:
|
||||
virtual future<authenticated_user> authenticate(const credentials_map& credentials) const override {
|
||||
auto i = credentials.find(authenticator::USERNAME_KEY);
|
||||
if ((i == credentials.end() || i->second.empty())
|
||||
&& (!credentials.contains(PASSWORD_KEY) || credentials.at(PASSWORD_KEY).empty())) {
|
||||
&& (!credentials.count(PASSWORD_KEY) || credentials.at(PASSWORD_KEY).empty())) {
|
||||
// return anon user
|
||||
return make_ready_future<authenticated_user>(anonymous_user());
|
||||
}
|
||||
@@ -158,7 +158,7 @@ public:
|
||||
}
|
||||
|
||||
virtual future<authenticated_user> get_authenticated_user() const {
|
||||
return futurize_invoke([this] {
|
||||
return futurize_apply([this] {
|
||||
return _sasl->get_authenticated_user().handle_exception([](auto ep) {
|
||||
try {
|
||||
std::rethrow_exception(ep);
|
||||
@@ -201,7 +201,7 @@ public:
|
||||
return _authorizer->stop();
|
||||
}
|
||||
|
||||
virtual std::string_view qualified_java_name() const override {
|
||||
virtual const sstring& qualified_java_name() const override {
|
||||
return transitional_authorizer_name();
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user